namespy 0.1.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 (156) hide show
  1. namespy-0.1.0/.gitattributes +27 -0
  2. namespy-0.1.0/.github/ISSUE_TEMPLATE/bug-report.yml +73 -0
  3. namespy-0.1.0/.github/ISSUE_TEMPLATE/config.yml +14 -0
  4. namespy-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +48 -0
  5. namespy-0.1.0/.github/dependabot.yml +34 -0
  6. namespy-0.1.0/.github/workflows/ci.yml +155 -0
  7. namespy-0.1.0/.github/workflows/release.yml +152 -0
  8. namespy-0.1.0/.github/workflows/security.yml +78 -0
  9. namespy-0.1.0/.github/workflows/socket.yml +37 -0
  10. namespy-0.1.0/.gitignore +72 -0
  11. namespy-0.1.0/AGENTS.md +52 -0
  12. namespy-0.1.0/CHANGELOG.md +1920 -0
  13. namespy-0.1.0/CONTRIBUTING.md +114 -0
  14. namespy-0.1.0/LICENSE +202 -0
  15. namespy-0.1.0/PKG-INFO +491 -0
  16. namespy-0.1.0/README.md +455 -0
  17. namespy-0.1.0/SECURITY.md +144 -0
  18. namespy-0.1.0/docs/HANDOFF.md +3969 -0
  19. namespy-0.1.0/docs/legacy/README.md +32 -0
  20. namespy-0.1.0/docs/legacy/database-schema.png +0 -0
  21. namespy-0.1.0/docs/legacy/schema.sql +162 -0
  22. namespy-0.1.0/docs/rfc-map.md +296 -0
  23. namespy-0.1.0/docs/roadmap.md +352 -0
  24. namespy-0.1.0/docs/strategy.md +120 -0
  25. namespy-0.1.0/examples/blocklist.py +160 -0
  26. namespy-0.1.0/examples/dynamic_responders.py +122 -0
  27. namespy-0.1.0/namespy/__init__.py +176 -0
  28. namespy-0.1.0/namespy/__main__.py +1727 -0
  29. namespy-0.1.0/namespy/aggressive.py +488 -0
  30. namespy-0.1.0/namespy/anchors.py +524 -0
  31. namespy-0.1.0/namespy/apps/__init__.py +60 -0
  32. namespy-0.1.0/namespy/apps/forwarder.py +275 -0
  33. namespy-0.1.0/namespy/apps/hosts.py +226 -0
  34. namespy-0.1.0/namespy/apps/lookup.py +319 -0
  35. namespy-0.1.0/namespy/apps/routing.py +203 -0
  36. namespy-0.1.0/namespy/apps/stub.py +257 -0
  37. namespy-0.1.0/namespy/bundle.py +352 -0
  38. namespy-0.1.0/namespy/cache/__init__.py +863 -0
  39. namespy-0.1.0/namespy/cache/sqlite.py +436 -0
  40. namespy-0.1.0/namespy/crypto.py +555 -0
  41. namespy-0.1.0/namespy/dnssec.py +1320 -0
  42. namespy-0.1.0/namespy/doh.py +431 -0
  43. namespy-0.1.0/namespy/edns.py +850 -0
  44. namespy-0.1.0/namespy/engine.py +554 -0
  45. namespy-0.1.0/namespy/enums.py +492 -0
  46. namespy-0.1.0/namespy/filtering.py +179 -0
  47. namespy-0.1.0/namespy/idna.py +361 -0
  48. namespy-0.1.0/namespy/maintenance.py +434 -0
  49. namespy-0.1.0/namespy/minimis.py +148 -0
  50. namespy-0.1.0/namespy/mqtype.py +320 -0
  51. namespy-0.1.0/namespy/named.root +92 -0
  52. namespy-0.1.0/namespy/names.py +587 -0
  53. namespy-0.1.0/namespy/nsec.py +1237 -0
  54. namespy-0.1.0/namespy/opportunistic.py +594 -0
  55. namespy-0.1.0/namespy/packet.py +934 -0
  56. namespy-0.1.0/namespy/py.typed +3 -0
  57. namespy-0.1.0/namespy/querylog.py +89 -0
  58. namespy-0.1.0/namespy/rbl.py +216 -0
  59. namespy-0.1.0/namespy/rdata.py +1919 -0
  60. namespy-0.1.0/namespy/resolver.py +5588 -0
  61. namespy-0.1.0/namespy/server.py +2377 -0
  62. namespy-0.1.0/namespy/special.py +149 -0
  63. namespy-0.1.0/namespy/stub.py +622 -0
  64. namespy-0.1.0/namespy/svcb.py +1891 -0
  65. namespy-0.1.0/namespy/trace.py +279 -0
  66. namespy-0.1.0/namespy/validate.py +1110 -0
  67. namespy-0.1.0/namespy/verify.py +519 -0
  68. namespy-0.1.0/namespy/wire.py +138 -0
  69. namespy-0.1.0/namespy/zone.py +780 -0
  70. namespy-0.1.0/pyproject.toml +192 -0
  71. namespy-0.1.0/tests/certs/dot-test-key.pem +5 -0
  72. namespy-0.1.0/tests/certs/dot-test.pem +9 -0
  73. namespy-0.1.0/tests/test_aggressive_nsec.py +433 -0
  74. namespy-0.1.0/tests/test_anchor_sentinel.py +289 -0
  75. namespy-0.1.0/tests/test_apps_forwarder.py +273 -0
  76. namespy-0.1.0/tests/test_apps_hosts.py +257 -0
  77. namespy-0.1.0/tests/test_apps_lookup.py +229 -0
  78. namespy-0.1.0/tests/test_apps_routing.py +148 -0
  79. namespy-0.1.0/tests/test_apps_stub.py +208 -0
  80. namespy-0.1.0/tests/test_bundle.py +425 -0
  81. namespy-0.1.0/tests/test_cache.py +403 -0
  82. namespy-0.1.0/tests/test_chain_cache.py +265 -0
  83. namespy-0.1.0/tests/test_cli.py +1337 -0
  84. namespy-0.1.0/tests/test_compact_denial.py +554 -0
  85. namespy-0.1.0/tests/test_crypto_backend.py +326 -0
  86. namespy-0.1.0/tests/test_dnssec_anchors.py +527 -0
  87. namespy-0.1.0/tests/test_dnssec_canonical.py +346 -0
  88. namespy-0.1.0/tests/test_dnssec_codecs.py +489 -0
  89. namespy-0.1.0/tests/test_dnssec_deprecation.py +233 -0
  90. namespy-0.1.0/tests/test_dnssec_ecc.py +592 -0
  91. namespy-0.1.0/tests/test_dnssec_proofs.py +510 -0
  92. namespy-0.1.0/tests/test_dnssec_rsa.py +173 -0
  93. namespy-0.1.0/tests/test_dnssec_serving.py +477 -0
  94. namespy-0.1.0/tests/test_dnssec_validate.py +1147 -0
  95. namespy-0.1.0/tests/test_dnssec_verify.py +535 -0
  96. namespy-0.1.0/tests/test_dnssec_wiring.py +1197 -0
  97. namespy-0.1.0/tests/test_dot_server.py +348 -0
  98. namespy-0.1.0/tests/test_dual_stack_policy.py +298 -0
  99. namespy-0.1.0/tests/test_dynamic_responders.py +298 -0
  100. namespy-0.1.0/tests/test_edns.py +652 -0
  101. namespy-0.1.0/tests/test_edns_error.py +299 -0
  102. namespy-0.1.0/tests/test_edns_keytag.py +362 -0
  103. namespy-0.1.0/tests/test_engine.py +537 -0
  104. namespy-0.1.0/tests/test_enums.py +266 -0
  105. namespy-0.1.0/tests/test_failure_cache.py +290 -0
  106. namespy-0.1.0/tests/test_failure_cache_resolver.py +690 -0
  107. namespy-0.1.0/tests/test_filtering.py +309 -0
  108. namespy-0.1.0/tests/test_fuzz_wire.py +455 -0
  109. namespy-0.1.0/tests/test_glue_policy.py +299 -0
  110. namespy-0.1.0/tests/test_idna.py +669 -0
  111. namespy-0.1.0/tests/test_key_tag_query.py +274 -0
  112. namespy-0.1.0/tests/test_minimis.py +93 -0
  113. namespy-0.1.0/tests/test_mqtype.py +367 -0
  114. namespy-0.1.0/tests/test_mqtype_interop.py +292 -0
  115. namespy-0.1.0/tests/test_names.py +83 -0
  116. namespy-0.1.0/tests/test_names_wire.py +241 -0
  117. namespy-0.1.0/tests/test_nsec_cache.py +467 -0
  118. namespy-0.1.0/tests/test_nsec_proofs.py +381 -0
  119. namespy-0.1.0/tests/test_nsec_responses.py +614 -0
  120. namespy-0.1.0/tests/test_nxdomain_cut.py +555 -0
  121. namespy-0.1.0/tests/test_opportunistic.py +1032 -0
  122. namespy-0.1.0/tests/test_packet.py +86 -0
  123. namespy-0.1.0/tests/test_packet_wire.py +613 -0
  124. namespy-0.1.0/tests/test_padding_policy.py +275 -0
  125. namespy-0.1.0/tests/test_public_api.py +85 -0
  126. namespy-0.1.0/tests/test_qname_min.py +225 -0
  127. namespy-0.1.0/tests/test_querylog.py +267 -0
  128. namespy-0.1.0/tests/test_rbl.py +295 -0
  129. namespy-0.1.0/tests/test_rdata_wrapping.py +154 -0
  130. namespy-0.1.0/tests/test_referral_glue.py +164 -0
  131. namespy-0.1.0/tests/test_resolve_steps.py +442 -0
  132. namespy-0.1.0/tests/test_resolver_alias.py +541 -0
  133. namespy-0.1.0/tests/test_resolver_classify.py +482 -0
  134. namespy-0.1.0/tests/test_resolver_e2e.py +387 -0
  135. namespy-0.1.0/tests/test_resolver_lifecycle.py +125 -0
  136. namespy-0.1.0/tests/test_resolver_matching.py +224 -0
  137. namespy-0.1.0/tests/test_resolver_mqtype.py +921 -0
  138. namespy-0.1.0/tests/test_resolver_truncation.py +340 -0
  139. namespy-0.1.0/tests/test_review_fixes.py +168 -0
  140. namespy-0.1.0/tests/test_serve_stale.py +319 -0
  141. namespy-0.1.0/tests/test_server.py +1562 -0
  142. namespy-0.1.0/tests/test_server_stream.py +356 -0
  143. namespy-0.1.0/tests/test_special_names.py +183 -0
  144. namespy-0.1.0/tests/test_stream_reuse.py +258 -0
  145. namespy-0.1.0/tests/test_stub_doh.py +457 -0
  146. namespy-0.1.0/tests/test_stub_dot.py +329 -0
  147. namespy-0.1.0/tests/test_stub_resolver.py +229 -0
  148. namespy-0.1.0/tests/test_svcb_presentation.py +474 -0
  149. namespy-0.1.0/tests/test_svcb_rdata.py +257 -0
  150. namespy-0.1.0/tests/test_svcb_wire.py +417 -0
  151. namespy-0.1.0/tests/test_trace_mermaid.py +213 -0
  152. namespy-0.1.0/tests/test_trace_text.py +130 -0
  153. namespy-0.1.0/tests/test_trust_maintenance.py +264 -0
  154. namespy-0.1.0/tests/test_wildcard_synthesis.py +394 -0
  155. namespy-0.1.0/tests/test_zone.py +462 -0
  156. namespy-0.1.0/tools/check_dist.py +133 -0
@@ -0,0 +1,27 @@
1
+ # Line endings are a correctness issue here, not a style one: CI runs the same
2
+ # suite on Ubuntu and Windows, and a checkout that rewrites bytes per platform
3
+ # means the formatter, the tests and the reviewer are looking at different
4
+ # files. The index stores LF; core.autocrlf is a per-machine setting that cannot
5
+ # be relied on to agree, so the policy is declared in the repository instead.
6
+ * text=auto eol=lf
7
+
8
+ # Shell must stay LF or it will not execute when checked out on Windows and
9
+ # copied into a Linux container.
10
+ *.sh text eol=lf
11
+
12
+ # Windows files that must keep CRLF to be useful as-is.
13
+ *.bat text eol=crlf
14
+ *.cmd text eol=crlf
15
+
16
+ # Never run end-of-line conversion or diff over these.
17
+ *.png binary
18
+ *.jpg binary
19
+ *.gif binary
20
+ *.ico binary
21
+ *.sqlite binary
22
+ *.db binary
23
+ *.pdf binary
24
+
25
+ # The root hints file is data with an exact on-the-wire meaning per RFC 1035
26
+ # section 5; keep its LF and treat edits to it as substantive.
27
+ namespy/named.root text eol=lf
@@ -0,0 +1,73 @@
1
+ name: Bug report
2
+ description: A crash, a wrong answer, or a divergence from a DNS specification
3
+ labels: ["bug"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ DNS bugs are usually invisible without the bytes. If something parsed
9
+ wrong, crashed, or produced an unexpected answer, **include the packet**
10
+ as a hex string — that single thing decides whether the report is
11
+ actionable.
12
+ - type: dropdown
13
+ id: area
14
+ attributes:
15
+ label: Which part?
16
+ options:
17
+ - Wire parsing (a crash or a wrong decode)
18
+ - Resolver behaviour (referrals, caching, aliases, glue)
19
+ - Server (`--serve`)
20
+ - CLI output
21
+ - Packaging / install
22
+ - Something else
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: what-happened
27
+ attributes:
28
+ label: What happened, and what did you expect?
29
+ description: >
30
+ For a parsing bug, paste the datagram as hex (`data.hex()`), plus the
31
+ offset in the error if one was raised. For a resolver bug, `namespy
32
+ --trace NAME` and the resulting `last_resolution.md` are usually enough
33
+ on their own.
34
+ render: shell
35
+ validations:
36
+ required: true
37
+ - type: textarea
38
+ id: repro
39
+ attributes:
40
+ label: Shortest reproduction
41
+ description: >
42
+ A few lines that show it, run against a clean checkout. Please prefer
43
+ `pip install -e ".[dev]"` at the commit you tested over a released
44
+ version, since this repository is not on PyPI.
45
+ render: python
46
+ validations:
47
+ required: true
48
+ - type: textarea
49
+ id: spec
50
+ attributes:
51
+ label: Specification
52
+ description: >
53
+ If you know which section this violates, cite it — e.g. "RFC 9267
54
+ section 5: RDLENGTH must be checked before the RDATA is read". If you do
55
+ not know, leave this empty and say so; guessing is still useful.
56
+ placeholder: RFC nnnn section n.n
57
+ - type: input
58
+ id: environment
59
+ attributes:
60
+ label: Environment
61
+ description: OS, Python version, and whether you ran with `-O` / `PYTHONOPTIMIZE`
62
+ placeholder: Windows 11, Python 3.12.7, no -O
63
+ validations:
64
+ required: true
65
+ - type: checkboxes
66
+ id: gates
67
+ attributes:
68
+ label: Checked the local gates?
69
+ description: These are the exact commands CI runs, so failing them here saves a round trip.
70
+ options:
71
+ - label: "`pytest -q` passes on a clean checkout"
72
+ - label: "`python -O -m pytest -q` also passes (assertions stripped)"
73
+ - label: "`ruff check .` and `mypy namespy` are clean"
@@ -0,0 +1,14 @@
1
+ blank_issues_enabled: true
2
+ contact_links:
3
+ - name: Report a security issue
4
+ url: https://github.com/b1tninja/namespy/security/advisories/new
5
+ about: >
6
+ Parser and resolver bugs are often remotely reachable from one malformed
7
+ datagram. Please use a private advisory, not a public issue — see
8
+ SECURITY.md.
9
+ - name: Spec and design questions
10
+ url: https://github.com/b1tninja/namespy/discussions
11
+ about: >
12
+ "Should this reject or tolerate that label?" and similar questions are
13
+ about which RFC reading we adopt. docs/rfc-map.md records the current
14
+ reading; a discussion is the right place to argue with it.
@@ -0,0 +1,48 @@
1
+ <!--
2
+ Delete any section that genuinely does not apply, but keep the gate list.
3
+ -->
4
+
5
+ ## What problem does this solve?
6
+
7
+ One or two sentences. If it fixes an issue, link it.
8
+
9
+ ## Behaviour change?
10
+
11
+ Say so explicitly if any of these moved, and update `CHANGELOG.md` under
12
+ Unreleased:
13
+
14
+ - [ ] A packet that used to parse now raises (or the reverse)
15
+ - [ ] A TTL, cache, or negative-answer decision changed
16
+ - [ ] A public name changed (module, class, enum member, CLI flag)
17
+ - [ ] Nothing observable changed
18
+
19
+ A parser that starts rejecting input is a *feature* here if the input was
20
+ malformed — RFC 5625 section 6.3 permits dropping a broken message and does not
21
+ require tolerating it. But it must be a decision, not a surprise.
22
+
23
+ ## Gates
24
+
25
+ These are the commands CI runs. Run them locally before pushing; the `-O` run is
26
+ the one that catches assertions carrying real validation, which is this codebase's
27
+ main historical failure mode.
28
+
29
+ ```bash
30
+ pytest -q # coverage floor enforced by pyproject
31
+ python -O -m pytest -q
32
+ ruff check . && ruff format --check .
33
+ mypy namespy
34
+ ```
35
+
36
+ - [ ] All four pass locally
37
+
38
+ ## Spec
39
+
40
+ Which document and section required this? If the change *is* a spec question, the
41
+ reading you chose belongs in `docs/rfc-map.md` — that file exists so the next
42
+ person does not have to re-litigate it.
43
+
44
+ ## Tests
45
+
46
+ Every parser change needs a byte-level test that pins the exact failure, not just
47
+ a round-trip. For a new rejection, name the condition in the test. For a new
48
+ acceptance, assert the decoded value, not merely that nothing raised.
@@ -0,0 +1,34 @@
1
+ version: 2
2
+ updates:
3
+ # Only the dev extra resolves here -- the library itself declares no runtime
4
+ # dependencies, which is a deliberate property of this codebase rather than an
5
+ # oversight. Keeping the toolchain fresh is still worth a pull request, because
6
+ # a stale ruff or pytest is what makes CI disagree with a local run.
7
+ - package-ecosystem: pip
8
+ directory: "/"
9
+ schedule:
10
+ interval: weekly
11
+ day: monday
12
+ open-pull-requests-limit: 5
13
+ commit-message:
14
+ prefix: "deps"
15
+ groups:
16
+ # One PR for the test/lint toolchain instead of five, since they are
17
+ # upgraded together or not at all.
18
+ dev-toolchain:
19
+ patterns:
20
+ - "pytest*"
21
+ - "ruff"
22
+ - "mypy"
23
+ - "hypothesis"
24
+ - "build"
25
+
26
+ # The actions themselves are third-party code executing with a repository
27
+ # token, so their pins get the same review as dependencies.
28
+ - package-ecosystem: github-actions
29
+ directory: "/"
30
+ schedule:
31
+ interval: weekly
32
+ day: monday
33
+ commit-message:
34
+ prefix: "ci"
@@ -0,0 +1,155 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ concurrency:
13
+ group: ci-${{ github.ref }}
14
+ cancel-in-progress: true
15
+
16
+ env:
17
+ # Hypothesis writes its counterexample database into the repo, which on a
18
+ # shared runner means a failure reproduced locally never reaches CI. Cached
19
+ # explicitly below instead of being thrown away with the rest of the checkout.
20
+ HYPOTHESIS_STORAGE_DIRECTORY: .hypothesis
21
+
22
+ jobs:
23
+ lint:
24
+ name: Lint
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@v7
28
+ - uses: actions/setup-python@v7
29
+ with:
30
+ python-version: "3.13"
31
+ cache: pip
32
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev]"
33
+
34
+ # `check` reports, `format --check` is the one that notices drift: a
35
+ # formatter that only runs on a developer's machine diverges silently and
36
+ # then produces whole-file diffs in the next unrelated commit.
37
+ - run: ruff check . --output-format=github
38
+ - run: ruff format --check .
39
+
40
+ # Blocking at the level the code is actually at: mypy namespy exits 0 today,
41
+ # so a check that merely printed the count would let regressions through
42
+ # while looking like a gate. --strict measures 274 errors, and 245 of them
43
+ # are no-untyped-def plus no-untyped-call -- one root cause, functions with
44
+ # no annotations, concentrated in resolver.py (132) rather than spread
45
+ # evenly. That is a per-module campaign, not a flag to flip: enabling
46
+ # strict globally would mean silencing the result, which is how a type
47
+ # checker stops being read.
48
+ - run: mypy namespy
49
+
50
+ # The optional backends, exercised where they exist. The Test matrix runs
51
+ # without a crypto backend -- that is the configuration most people install, and the
52
+ # no-backend path is what must stay clean -- so without this job the [crypto] extra
53
+ # would ship untested and the integration points its transports rest on would be
54
+ # assumptions. The [doh] extra (pure Python, no wheels to advise against) does ride
55
+ # the matrix, because an optional transport the matrix cannot run is a transport the
56
+ # matrix cannot cover either: the TLS fixtures are static stdlib material, so with
57
+ # h2 present every DoT and DoH line is exercised in all four matrix jobs -- and the
58
+ # coverage floor is the proof: when the dot/doh tests skipped for lack of a
59
+ # certificate generator, doh.py measured 15% and the floor broke the commit.
60
+ crypto_extra:
61
+ name: Crypto extra
62
+ runs-on: ubuntu-latest
63
+ steps:
64
+ - uses: actions/checkout@v7
65
+ - uses: actions/setup-python@v7
66
+ with:
67
+ python-version: "3.13"
68
+ cache: pip
69
+ # Resolving the extras is itself the check: it fails if a floor cannot be satisfied
70
+ # by a wheel on a supported platform, which is the failure mode an optional
71
+ # dependency has that no unit test can see.
72
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev,crypto,doh]"
73
+ - run: python -c "import cryptography, namespy.crypto as c; b=c.require_backend('probe'); print(b.name, b.version)"
74
+ - run: python -m pytest -q tests/test_crypto_backend.py
75
+ - run: python -c "import h2, namespy.doh as d; assert d.H2_AVAILABLE; print('h2', h2.__version__)"
76
+ - run: python -m pytest -q tests/test_stub_doh.py
77
+
78
+ test:
79
+ name: Test py${{ matrix.python-version }} on ${{ matrix.os }}
80
+ runs-on: ${{ matrix.os }}
81
+ strategy:
82
+ fail-fast: false
83
+ matrix:
84
+ # Windows is not decoration. The bundled server used to call
85
+ # setsockopt(IPPROTO_IP, IP_MTU_DISCOVER, ...) unconditionally with
86
+ # hardcoded Linux constants, which raised on Windows in connection_made
87
+ # -- a bug that no Linux-only CI could ever see. asyncio also swaps to
88
+ # ProactorEventLoop here, so socket lifetime differs by platform.
89
+ os: [ubuntu-latest, windows-latest]
90
+ python-version: ["3.12", "3.13"]
91
+ steps:
92
+ - uses: actions/checkout@v7
93
+ - uses: actions/setup-python@v7
94
+ with:
95
+ python-version: ${{ matrix.python-version }}
96
+ cache: pip
97
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev,doh]"
98
+
99
+ # The coverage floor is fail_under in pyproject.toml, not duplicated here,
100
+ # so the threshold and the explanation for it stay in one file. The --cov
101
+ # flag is what makes pytest read that file: pytest-cov measures only when
102
+ # asked, so this step exited 0 whatever the tests covered and the floor was
103
+ # decoration: the floor (50012b4) and the CI that never asked for a
104
+ # measurement (ecedb5c) both landed 2026-10-27. Checked both ways: one test
105
+ # file now exits 1 with "Required test coverage of 80.0% not reached", the
106
+ # whole suite exits 0 at 87%.
107
+ - name: pytest
108
+ run: python -m pytest -q --cov
109
+
110
+ # This step is the reason the suite is worth running at all here. Every
111
+ # bounds check on a wire path was once an `assert`, which python -O strips
112
+ # out of the bytecode; the parser's hardening is therefore only real if the
113
+ # hostile-input tests still pass with assertions compiled away. Without
114
+ # this leg, "no assert on network data" is a comment rather than a policy.
115
+ - name: pytest under -O (assertions stripped)
116
+ run: python -O -m pytest -q -p no:cacheprovider
117
+
118
+ - uses: actions/upload-artifact@v7
119
+ if: failure()
120
+ with:
121
+ name: pytest-${{ matrix.os }}-py${{ matrix.python-version }}
122
+ path: |
123
+ .hypothesis/
124
+ last_resolution.md
125
+ if-no-files-found: ignore
126
+
127
+ dist:
128
+ name: Build and audit distributions
129
+ runs-on: ubuntu-latest
130
+ steps:
131
+ - uses: actions/checkout@v7
132
+ - uses: actions/setup-python@v7
133
+ with:
134
+ python-version: "3.13"
135
+ cache: pip
136
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev]"
137
+ - run: python -m build
138
+ - run: python tools/check_dist.py dist/
139
+ # Installs the wheel in a clean venv and asserts the root hints arrive
140
+ # through importlib.resources -- the one failure mode unit tests cannot
141
+ # see, because they always run against the source tree.
142
+ #
143
+ # The `cd /` is load-bearing: the checkout contains an namespy/ directory,
144
+ # so importing from the workspace would silently import the source tree and
145
+ # "pass" no matter what the wheel actually contains.
146
+ - name: Install the wheel and load hints from it
147
+ run: |
148
+ python -m venv /tmp/verify
149
+ /tmp/verify/bin/python -m pip install --no-deps dist/*.whl
150
+ cd /
151
+ /tmp/verify/bin/python -c "import namespy, importlib.resources as r; print(len(r.files('namespy').joinpath('named.root').read_text().splitlines()), 'root hint lines')"
152
+ - uses: actions/upload-artifact@v7
153
+ with:
154
+ name: dist
155
+ path: dist/
@@ -0,0 +1,152 @@
1
+ name: Release
2
+
3
+ # Publishing is deliberately tag-gated rather than push-to-master: this project
4
+ # has never been on PyPI, and the first upload should be an explicit act with a
5
+ # version that exists in the tree. Cut a release with:
6
+ #
7
+ # git tag v0.1.0 && git push origin v0.1.0
8
+ #
9
+ # The tag must agree with namespy.__init__.__version__, which is what hatchling
10
+ # reads for the wheel's own version -- an unforced mismatch would put a name on
11
+ # PyPI that no source tree anywhere claims to be.
12
+
13
+ on:
14
+ push:
15
+ tags: ["v*"]
16
+ workflow_dispatch:
17
+ inputs:
18
+ version:
19
+ description: "Release this version; must equal namespy.__version__ (e.g. 0.1.0)"
20
+ required: true
21
+ type: string
22
+
23
+ permissions:
24
+ contents: read
25
+
26
+ jobs:
27
+ build:
28
+ name: Build release artifacts
29
+ runs-on: ubuntu-latest
30
+ # id-token + attestations are for the provenance step below; everything
31
+ # else in this job stays under contents: read.
32
+ permissions:
33
+ contents: read
34
+ id-token: write
35
+ attestations: write
36
+ artifact-metadata: write
37
+ outputs:
38
+ digest: ${{ steps.hash.outputs.digest }}
39
+ steps:
40
+ - uses: actions/checkout@v7
41
+ - uses: actions/setup-python@v7
42
+ with:
43
+ python-version: "3.13"
44
+ cache: pip
45
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev]"
46
+
47
+ - name: Refuse a version that disagrees with the package
48
+ env:
49
+ # Passed through the environment rather than interpolated into the
50
+ # script: an input is data, not shell text, even a maintainer's.
51
+ INPUT_VERSION: ${{ inputs.version }}
52
+ run: |
53
+ # A dispatched run carries no tag, so GITHUB_REF_NAME would be a
54
+ # branch name and the comparison would always fail; the input stands
55
+ # in for the tag. The rule itself is the same either way: what lands
56
+ # on PyPI must be a version some source tree actually claims.
57
+ if [ "${GITHUB_EVENT_NAME}" = "workflow_dispatch" ]; then
58
+ WANT="${INPUT_VERSION}"
59
+ else
60
+ WANT="${GITHUB_REF_NAME#v}"
61
+ fi
62
+ ACTUAL="$(python -c 'import namespy; print(namespy.__version__)')"
63
+ if [ "$WANT" != "$ACTUAL" ]; then
64
+ echo "release says $WANT but namespy.__version__ is $ACTUAL" >&2
65
+ exit 1
66
+ fi
67
+
68
+ - run: python -m build
69
+ - run: python tools/check_dist.py dist/
70
+
71
+ # GitHub-side SLSA provenance, signed by Sigstore against this run's OIDC
72
+ # identity: `gh attestation verify` ties the shipped files back to this
73
+ # repo and commit. PyPI receives its own PEP 740 attestations from the
74
+ # publish action; two independent proofs of the same build.
75
+ - name: Attest build provenance
76
+ uses: actions/attest@v4
77
+ with:
78
+ subject-path: dist/*
79
+
80
+ # SLSA provenance attribute: the SHA256 of every artifact the publish job
81
+ # will upload. Recorded here so a later audit can tie a file on PyPI back
82
+ # to this exact run without trusting the PyPI entry.
83
+ - name: Collect artifact digests
84
+ id: hash
85
+ run: |
86
+ cd dist && LANG=C LC_ALL=C sha256sum * | sort | sed 's/ * */ /' \
87
+ | base64 -w0 > ../digest.b64
88
+ echo "digest=$(cat ../digest.b64)" >> "$GITHUB_OUTPUT"
89
+
90
+ - uses: actions/upload-artifact@v7
91
+ with:
92
+ name: release-dist
93
+ path: dist/
94
+
95
+ publish:
96
+ name: Publish to PyPI
97
+ needs: build
98
+ runs-on: ubuntu-latest
99
+ # Trusted publishing (OIDC) rather than a stored API token: no long-lived
100
+ # credential exists to leak, and the audience/environment gate means only
101
+ # this repository's tagged builds can mint an upload credential.
102
+ #
103
+ # The `pypi` environment exists in Settings -> Environments; the job
104
+ # names it, so only runs from this repo/workflow can mint its token.
105
+ # The remaining half is on PyPI: the project `namespy` must list this
106
+ # repo, this workflow file and this environment name as a trusted
107
+ # publisher before any upload will be accepted. Until then a misfired
108
+ # tag fails at `twine` with a legible OIDC error rather than publishing.
109
+ environment:
110
+ name: pypi
111
+ url: https://pypi.org/p/namespy
112
+ permissions:
113
+ id-token: write
114
+ steps:
115
+ - uses: actions/download-artifact@v8
116
+ with:
117
+ name: release-dist
118
+ path: dist/
119
+ - uses: actions/setup-python@v7
120
+ with:
121
+ python-version: "3.13"
122
+ - run: python -m pip install --upgrade twine
123
+ - run: python -m twine check dist/*
124
+ - uses: pypa/gh-action-pypi-publish@release/v1
125
+ with:
126
+ print-hash: true
127
+ # PEP 740 digital attestations are generated and uploaded by the
128
+ # action automatically for trusted publishing; there is nothing to
129
+ # enable here.
130
+
131
+ release:
132
+ name: Create the GitHub Release
133
+ needs: [build, publish]
134
+ # A dispatched run has no tag to cut a release from: it is a rehearsal of
135
+ # the build/publish path and stays out of the releases list.
136
+ if: github.event_name == 'push'
137
+ runs-on: ubuntu-latest
138
+ permissions:
139
+ contents: write
140
+ steps:
141
+ - uses: actions/download-artifact@v8
142
+ with:
143
+ name: release-dist
144
+ path: dist/
145
+ # The GitHub Release is the human-facing artifact page, and it only
146
+ # appears after PyPI accepted the files: a failed publish leaves no
147
+ # half-release pointing at binaries that are not on PyPI.
148
+ - name: Publish the release
149
+ run: gh release create "$GITHUB_REF_NAME" dist/* --generate-notes
150
+ env:
151
+ GH_TOKEN: ${{ github.token }}
152
+
@@ -0,0 +1,78 @@
1
+ name: Security
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+ schedule:
8
+ # Weekly even without pushes, so a new CodeQL rule or a newly disclosed
9
+ # advisory surfaces against this code. Push-triggered analysis only ever
10
+ # asks about today's code with today's tools.
11
+ - cron: "17 3 * * 1"
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ jobs:
18
+ codeql:
19
+ name: CodeQL (${{ matrix.language }})
20
+ runs-on: ubuntu-latest
21
+ timeout-minutes: 360
22
+ permissions:
23
+ contents: read
24
+ security-events: write
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ # Only Python source here ships logic; the workflow files themselves
29
+ # are covered by review + Dependabot, not by a query pack.
30
+ language: [python]
31
+ steps:
32
+ - uses: actions/checkout@v7
33
+ - uses: actions/setup-python@v7
34
+ with:
35
+ python-version: "3.13"
36
+ - uses: github/codeql-action/init@v4
37
+ with:
38
+ languages: ${{ matrix.language }}
39
+ # security-extended over security-and-quality: this is a parser
40
+ # library whose real risk is hostile input, and the extended pack is
41
+ # the one that queries for it. The quality queries are already
42
+ # ruff's job and are enforced in ci.yml.
43
+ queries: security-extended
44
+ - uses: github/codeql-action/autobuild@v4
45
+ - uses: github/codeql-action/analyze@v4
46
+ with:
47
+ category: "/language:${{ matrix.language }}"
48
+
49
+ dependency-review:
50
+ # Diff-aware: it flags a newly *introduced* dependency on the PR that
51
+ # adds it, which no post-merge scanner does at the moment it matters.
52
+ # Only meaningful on pull_request, where there is a base to diff against.
53
+ if: github.event_name == 'pull_request'
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ contents: read
57
+ pull-requests: write
58
+ steps:
59
+ - uses: actions/checkout@v7
60
+ - uses: actions/dependency-review-action@v5
61
+ with:
62
+ fail-on-severity: high
63
+ comment-summary-in-pr: always
64
+
65
+ pip-audit:
66
+ # Audits the fully resolved dev-extra environment against the PyPA
67
+ # advisory DB. The library itself declares zero runtime dependencies, so
68
+ # what this can actually catch lives in the toolchain that builds, tests
69
+ # and publishes it -- which is where a Python supply-chain attack lands.
70
+ runs-on: ubuntu-latest
71
+ steps:
72
+ - uses: actions/checkout@v7
73
+ - uses: actions/setup-python@v7
74
+ with:
75
+ python-version: "3.13"
76
+ cache: pip
77
+ - run: python -m pip install --upgrade pip && pip install -e ".[dev]" pip-audit
78
+ - run: python -m pip_audit
@@ -0,0 +1,37 @@
1
+ name: Socket
2
+
3
+ on:
4
+ push:
5
+ branches: [master]
6
+ pull_request:
7
+
8
+ permissions:
9
+ contents: read
10
+
11
+ concurrency:
12
+ group: socket-${{ github.ref }}
13
+ cancel-in-progress: true
14
+
15
+ jobs:
16
+ firewall:
17
+ name: Screen dependency installs (Socket Firewall)
18
+ runs-on: ubuntu-latest
19
+ steps:
20
+ - uses: actions/checkout@v7
21
+ # setup-python deliberately comes *before* Socket (per Socket's own
22
+ # ordering guidance) and the pip step deliberately does not use
23
+ # actions/setup-python's cache: a cached wheel install never talks to
24
+ # the index, so nothing gets screened and the job would green-light a
25
+ # compromised release.
26
+ - uses: actions/setup-python@v7
27
+ with:
28
+ python-version: "3.13"
29
+ # Pinned to the immutable tag Socket's own docs recommend; Dependabot
30
+ # (github-actions ecosystem) keeps it current.
31
+ - uses: SocketDev/action@v1.3.2
32
+ with:
33
+ mode: firewall-free
34
+ # The point of the job: every dev dependency crosses the firewall on
35
+ # its way in. The library has no runtime dependencies to screen; the
36
+ # dev extra is the real attack surface.
37
+ - run: sfw pip install -e ".[dev]"