netprotocols 1.2.0__tar.gz → 2.0.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.
Files changed (148) hide show
  1. netprotocols-2.0.0/.github/ISSUE_TEMPLATE/bug_report.yml +60 -0
  2. netprotocols-2.0.0/.github/ISSUE_TEMPLATE/config.yml +10 -0
  3. netprotocols-2.0.0/.github/ISSUE_TEMPLATE/protocol_request.yml +66 -0
  4. netprotocols-2.0.0/.github/pull_request_template.md +40 -0
  5. netprotocols-2.0.0/.github/workflows/ci.yml +91 -0
  6. netprotocols-2.0.0/.github/workflows/release.yml +61 -0
  7. {netprotocols-1.2.0 → netprotocols-2.0.0}/.gitignore +3 -0
  8. netprotocols-2.0.0/ARCHITECTURE.md +457 -0
  9. netprotocols-2.0.0/CHANGELOG.md +744 -0
  10. netprotocols-2.0.0/CONTRIBUTING.md +143 -0
  11. netprotocols-2.0.0/PKG-INFO +330 -0
  12. netprotocols-2.0.0/README.md +305 -0
  13. netprotocols-2.0.0/benchmarks/baseline.json +13 -0
  14. netprotocols-2.0.0/docs/CLAIMS.md +680 -0
  15. {netprotocols-1.2.0 → netprotocols-2.0.0}/pyproject.toml +10 -1
  16. netprotocols-2.0.0/scripts/benchmark.py +449 -0
  17. netprotocols-2.0.0/scripts/capture_fixtures_dhcp.sh +94 -0
  18. netprotocols-2.0.0/scripts/capture_fixtures_dns_tcp.sh +112 -0
  19. netprotocols-2.0.0/scripts/capture_fixtures_gre.sh +112 -0
  20. netprotocols-2.0.0/scripts/capture_fixtures_vlan.sh +217 -0
  21. {netprotocols-1.2.0 → netprotocols-2.0.0}/scripts/check_fixtures.py +44 -7
  22. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/__init__.py +48 -6
  23. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/_base.py +42 -4
  24. netprotocols-2.0.0/src/netprotocols/_defaults.py +105 -0
  25. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/_enums.py +8 -0
  26. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/checksum.py +30 -4
  27. netprotocols-2.0.0/src/netprotocols/flow.py +96 -0
  28. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/layer2/__init__.py +2 -1
  29. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/layer2/arp.py +35 -15
  30. netprotocols-2.0.0/src/netprotocols/layer2/ethernet.py +97 -0
  31. netprotocols-2.0.0/src/netprotocols/layer2/vlan.py +103 -0
  32. netprotocols-2.0.0/src/netprotocols/layer3/__init__.py +28 -0
  33. netprotocols-2.0.0/src/netprotocols/layer3/gre.py +162 -0
  34. netprotocols-2.0.0/src/netprotocols/layer3/icmp.py +392 -0
  35. netprotocols-2.0.0/src/netprotocols/layer3/igmp.py +292 -0
  36. netprotocols-2.0.0/src/netprotocols/layer3/ip.py +460 -0
  37. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/layer3/ipv6_ext.py +136 -17
  38. netprotocols-2.0.0/src/netprotocols/layer4/__init__.py +4 -0
  39. netprotocols-2.0.0/src/netprotocols/layer4/_ports.py +66 -0
  40. netprotocols-2.0.0/src/netprotocols/layer4/tcp.py +326 -0
  41. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/layer4/udp.py +6 -4
  42. netprotocols-2.0.0/src/netprotocols/layer7/__init__.py +4 -0
  43. netprotocols-2.0.0/src/netprotocols/layer7/dhcp.py +299 -0
  44. netprotocols-2.0.0/src/netprotocols/layer7/dns.py +554 -0
  45. netprotocols-2.0.0/src/netprotocols/packet.py +174 -0
  46. netprotocols-2.0.0/src/netprotocols/registry.py +385 -0
  47. netprotocols-2.0.0/src/netprotocols/utils/exceptions.py +118 -0
  48. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/utils/ipv4.py +19 -3
  49. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/utils/mac.py +22 -4
  50. netprotocols-2.0.0/src/netprotocols/walk.py +173 -0
  51. netprotocols-2.0.0/tests/fixtures/MANIFEST.md +96 -0
  52. netprotocols-2.0.0/tests/fixtures/dhcp.pcap +0 -0
  53. netprotocols-2.0.0/tests/fixtures/dns_tcp.pcap +0 -0
  54. netprotocols-2.0.0/tests/fixtures/gre.pcap +0 -0
  55. netprotocols-2.0.0/tests/fixtures/staging/dhcp.pcap +0 -0
  56. netprotocols-2.0.0/tests/fixtures/staging/dns_tcp.pcap +0 -0
  57. netprotocols-2.0.0/tests/fixtures/staging/gre.pcap +0 -0
  58. netprotocols-2.0.0/tests/fixtures/staging/vlan_icmp.pcap +0 -0
  59. netprotocols-2.0.0/tests/fixtures/vlan_icmp.pcap +0 -0
  60. netprotocols-2.0.0/tests/strategies.py +274 -0
  61. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_arp.py +16 -0
  62. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_checksum.py +82 -3
  63. netprotocols-2.0.0/tests/test_contract.py +217 -0
  64. netprotocols-2.0.0/tests/test_corpus.py +311 -0
  65. netprotocols-2.0.0/tests/test_dhcp.py +260 -0
  66. netprotocols-2.0.0/tests/test_diagnostics.py +552 -0
  67. netprotocols-2.0.0/tests/test_dns.py +547 -0
  68. netprotocols-2.0.0/tests/test_docs.py +115 -0
  69. netprotocols-2.0.0/tests/test_exports.py +82 -0
  70. netprotocols-2.0.0/tests/test_flow.py +174 -0
  71. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_fuzz.py +88 -0
  72. netprotocols-2.0.0/tests/test_gre.py +245 -0
  73. netprotocols-2.0.0/tests/test_icmp.py +265 -0
  74. netprotocols-2.0.0/tests/test_igmp.py +329 -0
  75. netprotocols-2.0.0/tests/test_ip.py +221 -0
  76. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_ipv6_ext.py +133 -0
  77. netprotocols-2.0.0/tests/test_packet.py +103 -0
  78. netprotocols-2.0.0/tests/test_pattern_matching.py +256 -0
  79. netprotocols-2.0.0/tests/test_registry.py +296 -0
  80. netprotocols-2.0.0/tests/test_tcp.py +204 -0
  81. netprotocols-2.0.0/tests/test_version.py +54 -0
  82. netprotocols-2.0.0/tests/test_vlan.py +107 -0
  83. netprotocols-2.0.0/tests/test_walk.py +414 -0
  84. netprotocols-2.0.0/tests/test_workflows.py +118 -0
  85. {netprotocols-1.2.0 → netprotocols-2.0.0}/uv.lock +27 -1
  86. netprotocols-1.2.0/.github/workflows/ci.yml +0 -47
  87. netprotocols-1.2.0/.github/workflows/release.yml +0 -18
  88. netprotocols-1.2.0/ARCHITECTURE.md +0 -280
  89. netprotocols-1.2.0/CHANGELOG.md +0 -119
  90. netprotocols-1.2.0/PKG-INFO +0 -179
  91. netprotocols-1.2.0/README.md +0 -154
  92. netprotocols-1.2.0/src/netprotocols/layer2/ethernet.py +0 -66
  93. netprotocols-1.2.0/src/netprotocols/layer3/__init__.py +0 -21
  94. netprotocols-1.2.0/src/netprotocols/layer3/icmp.py +0 -135
  95. netprotocols-1.2.0/src/netprotocols/layer3/igmp.py +0 -95
  96. netprotocols-1.2.0/src/netprotocols/layer3/ip.py +0 -306
  97. netprotocols-1.2.0/src/netprotocols/layer4/__init__.py +0 -4
  98. netprotocols-1.2.0/src/netprotocols/layer4/_ports.py +0 -34
  99. netprotocols-1.2.0/src/netprotocols/layer4/tcp.py +0 -153
  100. netprotocols-1.2.0/src/netprotocols/layer7/__init__.py +0 -3
  101. netprotocols-1.2.0/src/netprotocols/layer7/dns.py +0 -234
  102. netprotocols-1.2.0/src/netprotocols/packet.py +0 -88
  103. netprotocols-1.2.0/src/netprotocols/utils/exceptions.py +0 -48
  104. netprotocols-1.2.0/tests/fixtures/MANIFEST.md +0 -51
  105. netprotocols-1.2.0/tests/test_contract.py +0 -104
  106. netprotocols-1.2.0/tests/test_corpus.py +0 -167
  107. netprotocols-1.2.0/tests/test_dns.py +0 -168
  108. netprotocols-1.2.0/tests/test_icmp.py +0 -55
  109. netprotocols-1.2.0/tests/test_igmp.py +0 -113
  110. netprotocols-1.2.0/tests/test_ip.py +0 -92
  111. netprotocols-1.2.0/tests/test_packet.py +0 -37
  112. netprotocols-1.2.0/tests/test_tcp.py +0 -80
  113. {netprotocols-1.2.0 → netprotocols-2.0.0}/LICENSE +0 -0
  114. {netprotocols-1.2.0 → netprotocols-2.0.0}/scripts/capture_fixtures.sh +0 -0
  115. {netprotocols-1.2.0 → netprotocols-2.0.0}/scripts/capture_fixtures_igmp.sh +0 -0
  116. {netprotocols-1.2.0 → netprotocols-2.0.0}/scripts/capture_fixtures_supplement.sh +0 -0
  117. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/py.typed +0 -0
  118. {netprotocols-1.2.0 → netprotocols-2.0.0}/src/netprotocols/utils/__init__.py +0 -0
  119. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/conftest.py +0 -0
  120. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/arp_exchange.pcap +0 -0
  121. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/icmpv4_echo_lo.pcap +0 -0
  122. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/icmpv4_external.pcap +0 -0
  123. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/icmpv4_ttl_exceeded.pcap +0 -0
  124. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/icmpv6_echo_lo.pcap +0 -0
  125. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/igmp.pcap +0 -0
  126. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/ipv4_fragments.pcap +0 -0
  127. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/ipv6_fragments.pcap +0 -0
  128. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/ipv6_mld.pcap +0 -0
  129. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/ipv6_ndp_mld.pcap +0 -0
  130. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/arp_exchange.pcap +0 -0
  131. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/icmpv4_echo_lo.pcap +0 -0
  132. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/icmpv4_external.pcap +0 -0
  133. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/icmpv4_ttl_exceeded.pcap +0 -0
  134. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/icmpv6_echo_lo.pcap +0 -0
  135. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/igmp.pcap +0 -0
  136. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/ipv4_fragments.pcap +0 -0
  137. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/ipv6_fragments.pcap +0 -0
  138. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/ipv6_mld.pcap +0 -0
  139. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/ipv6_ndp_mld.pcap +0 -0
  140. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/tcp_http.pcap +0 -0
  141. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/tcp_https.pcap +0 -0
  142. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/staging/udp_dns.pcap +0 -0
  143. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/tcp_http.pcap +0 -0
  144. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/tcp_https.pcap +0 -0
  145. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/fixtures/udp_dns.pcap +0 -0
  146. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_ethernet.py +0 -0
  147. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_udp.py +0 -0
  148. {netprotocols-1.2.0 → netprotocols-2.0.0}/tests/test_utils.py +0 -0
@@ -0,0 +1,60 @@
1
+ name: Bug report
2
+ description: A protocol decodes, encodes, or chains incorrectly, or raises the wrong error.
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for the report. netprotocols decodes real traffic, so the most
9
+ useful reports come with the actual bytes that trigger the problem.
10
+ Please search [existing issues](https://github.com/EONRaider/NETProtocols/issues)
11
+ first.
12
+ - type: input
13
+ id: version
14
+ attributes:
15
+ label: Version
16
+ description: >-
17
+ netprotocols version
18
+ (`python -c "import netprotocols; print(netprotocols.__version__)"`)
19
+ and your Python version.
20
+ placeholder: "netprotocols 1.2.0, Python 3.12.3"
21
+ validations:
22
+ required: true
23
+ - type: textarea
24
+ id: what-happened
25
+ attributes:
26
+ label: What happened
27
+ description: What did you decode or build, and what went wrong?
28
+ placeholder: "IPv4.decode(...) returned X, but the header on the wire says Y."
29
+ validations:
30
+ required: true
31
+ - type: textarea
32
+ id: repro
33
+ attributes:
34
+ label: Reproduction
35
+ description: >-
36
+ A minimal snippet. Include the raw bytes as a hex string so it runs
37
+ standalone — that is by far the fastest path to a fix. Rendered as
38
+ Python; no backticks needed.
39
+ render: python
40
+ placeholder: |
41
+ from netprotocols import Ethernet
42
+
43
+ frame = bytes.fromhex("ffffffffffff0007...")
44
+ print(Ethernet.decode(frame))
45
+ validations:
46
+ required: true
47
+ - type: textarea
48
+ id: expected
49
+ attributes:
50
+ label: Expected behavior
51
+ description: What should have happened instead?
52
+ validations:
53
+ required: true
54
+ - type: checkboxes
55
+ id: checks
56
+ attributes:
57
+ label: Checklist
58
+ options:
59
+ - label: I included the raw bytes (or a pcap) needed to reproduce this.
60
+ - label: I searched existing issues for a duplicate.
@@ -0,0 +1,10 @@
1
+ # Freeform issues stay enabled (the roadmap and design discussions don't
2
+ # fit a form); the templates just make the common cases structured.
3
+ blank_issues_enabled: true
4
+ contact_links:
5
+ - name: Contributing guide
6
+ url: https://github.com/EONRaider/NETProtocols/blob/master/CONTRIBUTING.md
7
+ about: Dev setup, the QA ladder, and how to add a protocol.
8
+ - name: Protocol roadmap
9
+ url: https://github.com/EONRaider/NETProtocols/issues/22
10
+ about: Planned protocols and their ordering — check before requesting one.
@@ -0,0 +1,66 @@
1
+ name: Protocol request
2
+ description: Request support for a protocol netprotocols does not decode yet.
3
+ labels: ["enhancement", "roadmap"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ The roadmap ([#22](https://github.com/EONRaider/NETProtocols/issues/22))
9
+ tracks planned protocols and their ordering — check it and existing
10
+ issues first. Adding a protocol follows the cookbook in
11
+ [ARCHITECTURE.md](https://github.com/EONRaider/NETProtocols/blob/master/ARCHITECTURE.md);
12
+ contributions are welcome.
13
+ - type: input
14
+ id: protocol
15
+ attributes:
16
+ label: Protocol
17
+ placeholder: "e.g. DHCP, GRE, 802.1X"
18
+ validations:
19
+ required: true
20
+ - type: input
21
+ id: spec
22
+ attributes:
23
+ label: Specification
24
+ description: The RFC or standard number(s) that define the header.
25
+ placeholder: "RFC 2131"
26
+ validations:
27
+ required: true
28
+ - type: dropdown
29
+ id: layer
30
+ attributes:
31
+ label: Layer
32
+ description: Roughly where it sits in the stack.
33
+ options:
34
+ - Link (2)
35
+ - Network (3)
36
+ - Transport (4)
37
+ - Application (7)
38
+ - Other / not sure
39
+ validations:
40
+ required: true
41
+ - type: textarea
42
+ id: dispatch
43
+ attributes:
44
+ label: How it is reached
45
+ description: >-
46
+ What carries it — which lower layer, and which dispatch value
47
+ (EtherType, IP protocol number, or port)?
48
+ placeholder: "Behind UDP on ports 67/68."
49
+ validations:
50
+ required: false
51
+ - type: textarea
52
+ id: sample
53
+ attributes:
54
+ label: Sample traffic
55
+ description: >-
56
+ A pcap or a hex dump of a real frame makes this far easier to
57
+ implement and test — the fixture corpus is captured from real traffic.
58
+ validations:
59
+ required: false
60
+ - type: checkboxes
61
+ id: checks
62
+ attributes:
63
+ label: Checklist
64
+ options:
65
+ - label: I checked the roadmap (#22) and existing issues.
66
+ - label: I can provide a real capture (pcap or hex) of this protocol.
@@ -0,0 +1,40 @@
1
+ <!--
2
+ Thanks for contributing to netprotocols! Fill in the sections below and
3
+ delete this comment. CONTRIBUTING.md has the full workflow; the short
4
+ version is: keep the QA ladder green and update the docs your change
5
+ touches. Relative links below resolve from the repo root once this
6
+ becomes the PR description.
7
+ -->
8
+
9
+ ## Summary
10
+
11
+ <!-- What does this change do, and why? Link any related issue, e.g. "Closes #22". -->
12
+
13
+ ## What's included
14
+
15
+ <!-- The concrete changes, one bullet each: new/edited modules, tests, docs. -->
16
+
17
+ -
18
+
19
+ ## Verification
20
+
21
+ <!-- Paste the QA-ladder output, or tick the boxes. All four gates run in CI. -->
22
+
23
+ - [ ] `uv run ruff check` and `uv run ruff format --check` are clean
24
+ - [ ] `uv run mypy` is clean (strict)
25
+ - [ ] `uv run pytest` passes locally (CI runs it on Python 3.12 / 3.13 / 3.14)
26
+ - [ ] `CHANGELOG.md` has an entry under `## [Unreleased]`
27
+
28
+ ### New protocol or dispatch change — also:
29
+
30
+ <!-- Delete this block if it doesn't apply. See ARCHITECTURE.md's cookbook. -->
31
+
32
+ - [ ] Followed the add-a-protocol cookbook in [ARCHITECTURE.md](ARCHITECTURE.md) and the decode contract in `src/netprotocols/_base.py`
33
+ - [ ] New `EtherType` / `IPProtocol` numbers carry display names in lockstep (a completeness test enforces it)
34
+ - [ ] Registered the class in the fuzz suite (`tests/test_fuzz.py::ALL_PROTOCOLS`)
35
+ - [ ] Added a fixture and updated [tests/fixtures/MANIFEST.md](tests/fixtures/MANIFEST.md) — a real capture where possible (see CONTRIBUTING.md for the capture scripts)
36
+ - [ ] Updated the README coverage table and the ARCHITECTURE.md chain diagram
37
+
38
+ ## Notes
39
+
40
+ <!-- Anything reviewers should know: trade-offs, follow-ups, companion PRs. Optional. -->
@@ -0,0 +1,91 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ branches: [master]
8
+ # Callable so the release workflow can run this exact ladder against a
9
+ # tagged commit before publishing (see release.yml). Keeping one
10
+ # definition means the release gate cannot drift from the PR gate.
11
+ workflow_call:
12
+
13
+ jobs:
14
+ lint:
15
+ runs-on: ubuntu-latest
16
+ steps:
17
+ - uses: actions/checkout@v5
18
+ - uses: astral-sh/setup-uv@v6
19
+ with:
20
+ python-version: "3.12"
21
+ - run: uv run ruff check
22
+ - run: uv run ruff format --check
23
+
24
+ typecheck:
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v5
28
+ - uses: astral-sh/setup-uv@v6
29
+ with:
30
+ python-version: "3.12"
31
+ - run: uv run mypy
32
+
33
+ test:
34
+ runs-on: ubuntu-latest
35
+ strategy:
36
+ matrix:
37
+ python-version: ["3.12", "3.13", "3.14"]
38
+ steps:
39
+ - uses: actions/checkout@v5
40
+ - uses: astral-sh/setup-uv@v6
41
+ with:
42
+ python-version: ${{ matrix.python-version }}
43
+ - run: uv run --frozen pytest --cov=netprotocols --cov-report=term
44
+
45
+ build:
46
+ runs-on: ubuntu-latest
47
+ steps:
48
+ - uses: actions/checkout@v5
49
+ - uses: astral-sh/setup-uv@v6
50
+ - run: uv build
51
+ - run: uv run --isolated --no-project --with dist/*.whl python -c "import netprotocols; print(netprotocols.__version__)"
52
+
53
+ # Blocking, at 15% below the committed baseline. The threshold has
54
+ # runner data behind it: this job's first run measured 3.7% away from
55
+ # a baseline recorded on entirely different hardware, and run-to-run
56
+ # spread on one machine is ~2%. 15% is therefore roughly four times
57
+ # the noise it has to tolerate, which leaves real regressions (Tier 1
58
+ # moved this number by 30-90% a step) nowhere to hide.
59
+ #
60
+ # A failure re-measures once before failing, because a shared runner
61
+ # can lose a slice of CPU to a neighbour: a genuine regression fails
62
+ # both attempts, a noisy one usually does not.
63
+ #
64
+ # Pinned to the baseline's interpreter on purpose: normalization
65
+ # cancels how fast the machine is, not which CPython it runs (see the
66
+ # module docstring in scripts/benchmark.py). If a change is a
67
+ # deliberate trade, re-record with --update-baseline and say why.
68
+ benchmark:
69
+ runs-on: ubuntu-latest
70
+ steps:
71
+ - uses: actions/checkout@v5
72
+ - uses: astral-sh/setup-uv@v6
73
+ with:
74
+ python-version: "3.12"
75
+ - name: Corpus decode throughput
76
+ shell: bash
77
+ run: |
78
+ # Keep the benchmark's own exit status: piping the whole thing
79
+ # into tee would report tee's, and wrapping it in a { } group
80
+ # would report the trailing echo's -- either way the gate
81
+ # would pass while the check failed.
82
+ status=0
83
+ uv run --frozen --group bench python scripts/benchmark.py \
84
+ --check --threshold 15 --compare >benchmark.out 2>&1 || status=$?
85
+ {
86
+ echo '```'
87
+ cat benchmark.out
88
+ echo '```'
89
+ } >>"$GITHUB_STEP_SUMMARY"
90
+ cat benchmark.out
91
+ exit "$status"
@@ -0,0 +1,61 @@
1
+ name: Release
2
+
3
+ on:
4
+ push:
5
+ tags: ["v*"]
6
+
7
+ permissions:
8
+ contents: read
9
+
10
+ jobs:
11
+ # Run the full QA ladder against the tagged commit before anything is
12
+ # uploaded. This is not redundant with the CI workflow's own triggers:
13
+ # those fire on pushes and pull requests targeting master, and a tag
14
+ # push matches neither, so without this job a release ran no checks at
15
+ # all. A tag can also point at a commit that never landed on master.
16
+ # A published version can only be yanked, never replaced.
17
+ ci:
18
+ uses: ./.github/workflows/ci.yml
19
+
20
+ publish:
21
+ needs: ci
22
+ runs-on: ubuntu-latest
23
+ environment: pypi
24
+ permissions:
25
+ id-token: write
26
+ contents: read
27
+ steps:
28
+ - uses: actions/checkout@v5
29
+ - uses: astral-sh/setup-uv@v6
30
+ - run: uv build
31
+ # The QA ladder cannot catch a mistagged release: every check
32
+ # passes because the code is fine and only the tag is wrong. Read
33
+ # the version back out of the artifact that is about to be
34
+ # uploaded, rather than re-parsing pyproject.toml, so this
35
+ # compares against what PyPI will actually receive.
36
+ - name: Verify the tag matches the version being published
37
+ run: |
38
+ set -euo pipefail
39
+ # Only the "v*" trigger keeps a bare "1.3.0" tag from reaching
40
+ # this step, where it would compare equal and pass. Check the
41
+ # prefix here too, so the contract survives a wider trigger.
42
+ case "${GITHUB_REF_NAME}" in
43
+ v*) tag="${GITHUB_REF_NAME#v}" ;;
44
+ *)
45
+ echo "::error::Tag ${GITHUB_REF_NAME} does not start with 'v'."
46
+ echo "Release tags must be of the form v<version>." >&2
47
+ exit 1
48
+ ;;
49
+ esac
50
+ built=$(uv run --isolated --no-project --with dist/*.whl \
51
+ python -c \
52
+ "from importlib.metadata import version; print(version('netprotocols'))")
53
+ if [ "$tag" != "$built" ]; then
54
+ echo "::error::Tag ${GITHUB_REF_NAME} would publish version ${built}."
55
+ echo "The tag and the packaged version must agree." >&2
56
+ echo "Either retag as v${built}, or set the version in" >&2
57
+ echo "pyproject.toml (and __version__) to ${tag} and retag." >&2
58
+ exit 1
59
+ fi
60
+ echo "Tag ${GITHUB_REF_NAME} matches packaged version ${built}."
61
+ - run: uv publish --trusted-publishing always
@@ -147,3 +147,6 @@ amass.log
147
147
 
148
148
  # Application-specific
149
149
  .logs/tests/fixtures/staging/
150
+
151
+ # Written by the benchmark CI step (scripts/benchmark.py output)
152
+ benchmark.out