ncarnate 2.2.2__tar.gz → 2.3.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 (185) hide show
  1. ncarnate-2.3.1/.gitattributes +2 -0
  2. ncarnate-2.3.1/.github/release-recovery-2.3.0.json +31 -0
  3. {ncarnate-2.2.2 → ncarnate-2.3.1}/.github/workflows/ci.yml +104 -14
  4. ncarnate-2.3.1/.github/workflows/full-granules.yml +69 -0
  5. ncarnate-2.3.1/.github/workflows/publish.yml +120 -0
  6. {ncarnate-2.2.2 → ncarnate-2.3.1}/.gitignore +6 -0
  7. {ncarnate-2.2.2 → ncarnate-2.3.1}/.readthedocs.yaml +1 -1
  8. ncarnate-2.3.1/.zenodo.json +26 -0
  9. {ncarnate-2.2.2 → ncarnate-2.3.1}/CHANGELOG.md +59 -0
  10. {ncarnate-2.2.2 → ncarnate-2.3.1}/CITATION.cff +5 -2
  11. ncarnate-2.3.1/CONTRIBUTING.md +98 -0
  12. {ncarnate-2.2.2 → ncarnate-2.3.1}/PKG-INFO +151 -36
  13. {ncarnate-2.2.2 → ncarnate-2.3.1}/README.md +141 -33
  14. ncarnate-2.3.1/brand/OFL.txt +90 -0
  15. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/README.md +21 -0
  16. ncarnate-2.3.1/brand/build_social_preview.py +78 -0
  17. ncarnate-2.3.1/brand/ncarnate-social-preview.png +0 -0
  18. ncarnate-2.3.1/brand/sora-600.ttf +0 -0
  19. ncarnate-2.3.1/docs/api.rst +190 -0
  20. ncarnate-2.3.1/docs/ci-dependencies.rst +81 -0
  21. {ncarnate-2.2.2 → ncarnate-2.3.1}/docs/fidelity-notes.md +41 -7
  22. {ncarnate-2.2.2 → ncarnate-2.3.1}/docs/index.rst +3 -0
  23. ncarnate-2.3.1/docs/release-notes.rst +5 -0
  24. ncarnate-2.3.1/docs/releasing.rst +142 -0
  25. ncarnate-2.3.1/examples/verified_conversion.py +37 -0
  26. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/__init__.py +6 -0
  27. ncarnate-2.3.1/ncarnate/_bounded_worker.py +38 -0
  28. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/atttypes.py +78 -13
  29. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/__init__.py +1 -0
  30. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/codes.py +19 -1
  31. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/models.py +29 -0
  32. ncarnate-2.3.1/ncarnate/checkpoints.py +152 -0
  33. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/constants.py +1 -1
  34. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/__init__.py +75 -28
  35. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/preflight.py +14 -125
  36. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/reader.py +5 -1
  37. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/report.py +1 -1
  38. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/core.py +151 -26
  39. ncarnate-2.3.1/ncarnate/destinations.py +57 -0
  40. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/eos/swath.py +43 -10
  41. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/handoff.py +141 -12
  42. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/hdf4.py +258 -40
  43. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/hdf4_runtime.py +2 -3
  44. ncarnate-2.3.1/ncarnate/hdf4_stream.py +485 -0
  45. ncarnate-2.3.1/ncarnate/journal.py +42 -0
  46. ncarnate-2.3.1/ncarnate/memory.py +216 -0
  47. ncarnate-2.3.1/ncarnate/prepared.py +186 -0
  48. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/result.py +81 -6
  49. ncarnate-2.3.1/ncarnate/streaming.py +98 -0
  50. {ncarnate-2.2.2 → ncarnate-2.3.1}/paper/paper.md +12 -2
  51. {ncarnate-2.2.2 → ncarnate-2.3.1}/pyproject.toml +19 -9
  52. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_audit_path.py +12 -0
  53. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_codes.py +4 -0
  54. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/conftest.py +3 -2
  55. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_agreement.py +1 -1
  56. ncarnate-2.3.1/tests/fixtures/corpus.json +60 -0
  57. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/myd05_trim.provenance.json +5 -1
  58. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/trim_hdfeos2.py +9 -2
  59. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_attribute_types.py +17 -7
  60. ncarnate-2.3.1/tests/test_contract_hardening.py +260 -0
  61. ncarnate-2.3.1/tests/test_conversion_reporting.py +87 -0
  62. ncarnate-2.3.1/tests/test_convert_file.py +239 -0
  63. ncarnate-2.3.1/tests/test_full_corpus.py +70 -0
  64. ncarnate-2.3.1/tests/test_hdf4_streaming.py +676 -0
  65. ncarnate-2.3.1/tests/test_memory_worker.py +158 -0
  66. ncarnate-2.3.1/tests/test_prepared.py +197 -0
  67. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_raw_granules.py +2 -2
  68. ncarnate-2.3.1/tests/test_release_review_regressions.py +179 -0
  69. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_sdist_contents.py +19 -2
  70. ncarnate-2.3.1/tests/test_streaming_cache.py +22 -0
  71. ncarnate-2.3.1/tests/test_swath_tiling.py +55 -0
  72. ncarnate-2.3.1/tools/corpus.py +125 -0
  73. ncarnate-2.3.1/tools/hatch_build.py +18 -0
  74. ncarnate-2.3.1/tools/test_distribution.py +75 -0
  75. ncarnate-2.3.1/tools/test_full_corpus.py +45 -0
  76. ncarnate-2.3.1/tools/tests/test_ci_helpers.py +86 -0
  77. ncarnate-2.3.1/tools/tests/test_corpus_downloads.py +175 -0
  78. ncarnate-2.3.1/tools/tests/test_release_retry.py +81 -0
  79. ncarnate-2.3.1/tools/validate_release_metadata.py +99 -0
  80. ncarnate-2.3.1/tools/verify_release_retry.py +67 -0
  81. ncarnate-2.2.2/.gitattributes +0 -2
  82. ncarnate-2.2.2/.github/workflows/publish.yml +0 -80
  83. ncarnate-2.2.2/.zenodo.json +0 -25
  84. ncarnate-2.2.2/CONTRIBUTING.md +0 -65
  85. ncarnate-2.2.2/conda-recipe/README.md +0 -54
  86. ncarnate-2.2.2/conda-recipe/recipe.yaml +0 -78
  87. ncarnate-2.2.2/docs/api.rst +0 -70
  88. {ncarnate-2.2.2 → ncarnate-2.3.1}/.github/FUNDING.yml +0 -0
  89. {ncarnate-2.2.2 → ncarnate-2.3.1}/CODE_OF_CONDUCT.md +0 -0
  90. {ncarnate-2.2.2 → ncarnate-2.3.1}/LICENSE +0 -0
  91. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/build_lockup.py +0 -0
  92. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/build_raster_lockup.py +0 -0
  93. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/compose_logo.py +0 -0
  94. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/globe.py +0 -0
  95. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-lockup-dark.png +0 -0
  96. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-lockup-dark.svg +0 -0
  97. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-lockup.png +0 -0
  98. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-lockup.svg +0 -0
  99. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-mark.png +0 -0
  100. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-mono.svg +0 -0
  101. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-source.png +0 -0
  102. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-white.svg +0 -0
  103. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-wordmark-white.svg +0 -0
  104. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate-wordmark.svg +0 -0
  105. {ncarnate-2.2.2 → ncarnate-2.3.1}/brand/ncarnate.svg +0 -0
  106. {ncarnate-2.2.2 → ncarnate-2.3.1}/docs/conf.py +0 -0
  107. {ncarnate-2.2.2 → ncarnate-2.3.1}/docs/requirements.txt +0 -0
  108. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/__main__.py +0 -0
  109. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/classify.py +0 -0
  110. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/inspect.py +0 -0
  111. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/audit/report.py +0 -0
  112. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/cli.py +0 -0
  113. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/integrity.py +0 -0
  114. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/convert/models.py +0 -0
  115. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/discovery.py +0 -0
  116. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/eos/__init__.py +0 -0
  117. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/eos/gctp.py +0 -0
  118. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/eos/grid.py +0 -0
  119. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/eos/structmeta.py +0 -0
  120. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/errors.py +0 -0
  121. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/formats.py +0 -0
  122. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/hashing.py +0 -0
  123. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/limits.py +0 -0
  124. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/schemas/__init__.py +0 -0
  125. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/schemas/handoff.schema.json +0 -0
  126. {ncarnate-2.2.2 → ncarnate-2.3.1}/ncarnate/stage.py +0 -0
  127. {ncarnate-2.2.2 → ncarnate-2.3.1}/paper/paper.bib +0 -0
  128. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/__init__.py +0 -0
  129. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/record.schema.json +0 -0
  130. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_agreement.py +0 -0
  131. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_checksum.py +0 -0
  132. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_classify.py +0 -0
  133. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_cli_dispatch.py +0 -0
  134. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_contract.py +0 -0
  135. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_emission.py +0 -0
  136. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_error_codes.py +0 -0
  137. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_inspect.py +0 -0
  138. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_models.py +0 -0
  139. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_readonly.py +0 -0
  140. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/audit/test_scaffold.py +0 -0
  141. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/__init__.py +0 -0
  142. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_cli_dispatch.py +0 -0
  143. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_cli_integration.py +0 -0
  144. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_convert_manifest.py +0 -0
  145. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_inplace_skip.py +0 -0
  146. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_integrity.py +0 -0
  147. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_models.py +0 -0
  148. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_reader.py +0 -0
  149. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/convert/test_summary_exit.py +0 -0
  150. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/blockers/compound_type.nc +0 -0
  151. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.hdf +0 -0
  152. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/amsre_5daysnow_trim.provenance.json +0 -0
  153. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.hdf +0 -0
  154. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/amsre_seaice12km_trim.provenance.json +0 -0
  155. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/mod03_trim.hdf +0 -0
  156. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/mod03_trim.provenance.json +0 -0
  157. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/myd05_trim.hdf +0 -0
  158. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/raingrid_trim.hdf +0 -0
  159. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/hdfeos2/raingrid_trim.provenance.json +0 -0
  160. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/netcdf/endianness.nc +0 -0
  161. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/netcdf/nested_groups.nc +0 -0
  162. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/netcdf/packed_fill.nc +0 -0
  163. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/data/netcdf/unlimited_dim.nc +0 -0
  164. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/make_blocker_fixtures.py +0 -0
  165. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/make_fixtures.py +0 -0
  166. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/operation_result/amsre_seaice12km.result.json +0 -0
  167. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/fixtures/operation_result/packed_fill.canonical.json +0 -0
  168. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_cli.py +0 -0
  169. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_convert_collisions.py +0 -0
  170. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_convert_inplace_collisions.py +0 -0
  171. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_discovery.py +0 -0
  172. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_eos_units.py +0 -0
  173. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_formats.py +0 -0
  174. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_g5_handoff_sufficiency.py +0 -0
  175. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_handoff_gate.py +0 -0
  176. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_handoff_schema.py +0 -0
  177. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_hdf4_conversion.py +0 -0
  178. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_limits.py +0 -0
  179. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_operation_result.py +0 -0
  180. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_optional_hdf4.py +0 -0
  181. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_plan_hash.py +0 -0
  182. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_recompress_netcdf.py +0 -0
  183. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_stage_api_4b.py +0 -0
  184. {ncarnate-2.2.2 → ncarnate-2.3.1}/tests/test_stage_api_g4.py +0 -0
  185. {ncarnate-2.2.2 → ncarnate-2.3.1}/tools/validate_distributions.py +0 -0
@@ -0,0 +1,2 @@
1
+ # Keep release source bytes consistent in Windows and Linux checkouts.
2
+ * text=auto eol=lf
@@ -0,0 +1,31 @@
1
+ {
2
+ "repository": "ErickShepherd/ncarnate",
3
+ "tag": "v2.3.0",
4
+ "run_id": 37859119484,
5
+ "head_sha": "7ae1c805944d9fd212d1ba268f4f83092304c8a3",
6
+ "required_jobs": [
7
+ "Test and build the exact release revision / Documentation and release metadata",
8
+ "Test and build the exact release revision / Installed Zarr companion",
9
+ "Test and build the exact release revision / Installed sdist (macos-latest)",
10
+ "Test and build the exact release revision / Installed sdist (ubuntu-latest)",
11
+ "Test and build the exact release revision / Installed sdist (windows-latest)",
12
+ "Test and build the exact release revision / Installed wheel (macos-latest)",
13
+ "Test and build the exact release revision / Installed wheel (ubuntu-latest)",
14
+ "Test and build the exact release revision / Installed wheel (windows-latest)",
15
+ "Test and build the exact release revision / build + twine check",
16
+ "Test and build the exact release revision / pytest (macos-latest py3.12)",
17
+ "Test and build the exact release revision / pytest (ubuntu-latest py3.10)",
18
+ "Test and build the exact release revision / pytest (ubuntu-latest py3.11)",
19
+ "Test and build the exact release revision / pytest (ubuntu-latest py3.12)",
20
+ "Test and build the exact release revision / pytest (ubuntu-latest py3.13)",
21
+ "Test and build the exact release revision / pytest (windows-latest py3.11)",
22
+ "Test and build the exact release revision / pytest degraded-capability (windows pip)",
23
+ "Test and build the exact release revision / pytest full-capability (conda-forge, ubuntu-latest)",
24
+ "Test and build the exact release revision / pytest full-capability (conda-forge, windows-latest)",
25
+ "Test and build the exact release revision / ruff"
26
+ ],
27
+ "files": {
28
+ "ncarnate-2.3.0-py3-none-any.whl": "5477b2825e27bfb52b9b44b6c5a6c169ea0344b9b7f44edcb58c8b1a277127f8",
29
+ "ncarnate-2.3.0.tar.gz": "3852afbea8e31697e5a75eb2fd266672fbf932b43aa58c3077c0c9c60f1617ce"
30
+ }
31
+ }
@@ -5,6 +5,17 @@ on:
5
5
  branches: [main]
6
6
  pull_request:
7
7
  workflow_dispatch:
8
+ workflow_call:
9
+ inputs:
10
+ release_tag:
11
+ type: string
12
+ default: ''
13
+ publish_target:
14
+ type: string
15
+ default: ''
16
+
17
+ permissions:
18
+ contents: read
8
19
 
9
20
  # The pip-based jobs (test, test-windows-degraded, build) install no system
10
21
  # HDF libraries on purpose: the netCDF4 wheels bundle libhdf5/libnetcdf, and
@@ -16,6 +27,20 @@ on:
16
27
  # the *supported* install channel delivers full HDF4 capability (KD-L7, G2).
17
28
 
18
29
  jobs:
30
+ docs-and-metadata:
31
+ name: Documentation and release metadata
32
+ runs-on: ubuntu-latest
33
+ steps:
34
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
35
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
36
+ with:
37
+ python-version: "3.12"
38
+ - run: python -m pip install -r docs/requirements.txt PyYAML cffconvert pytest
39
+ - run: python -m pytest -q tools/tests
40
+ - run: python tools/validate_release_metadata.py
41
+ - run: cffconvert --validate
42
+ - run: python -m sphinx -W --keep-going -b html docs docs/_build/html
43
+
19
44
  lint:
20
45
  name: ruff
21
46
  runs-on: ubuntu-latest
@@ -41,15 +66,13 @@ jobs:
41
66
  os: [ubuntu-latest]
42
67
  python-version: ["3.10", "3.11", "3.12", "3.13"]
43
68
  # Smoke the self-contained-wheel install story (pyhdf + netCDF4 +
44
- # pyproj, no system HDF libs) on macOS too. Windows is deliberately
45
- # excluded from the *full* matrix: pyhdf's Windows wheel ships no
46
- # HDF4 runtime, so the HDF4 fixtures can't run there. Windows-pip
47
- # instead gets the dedicated degraded-capability job below
48
- # (KD-L4/KD-L7), which pins exactly what must keep working — and
49
- # exactly how the HDF4 path must refuse — without a usable pyhdf.
69
+ # pyproj, no system HDF libs) on macOS and Windows. A separate
70
+ # injected-missing-runtime job retains degraded-capability coverage.
50
71
  include:
51
72
  - os: macos-latest
52
73
  python-version: "3.12"
74
+ - os: windows-latest
75
+ python-version: "3.11"
53
76
  steps:
54
77
  - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
55
78
  - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
@@ -71,9 +94,8 @@ jobs:
71
94
  with:
72
95
  python-version: "3.12"
73
96
  - name: Install package with test extras (pip wheels only)
74
- # pyhdf's Windows wheel *installs* but cannot *import* (it ships no
75
- # HDF4 runtime) — which is precisely the degraded environment this
76
- # job exists to pin (KD-L4/KD-L7, gate G2).
97
+ # Tests inject an unavailable runtime, independently of whether the
98
+ # installed pyhdf wheel works on this particular Windows runner.
77
99
  run: |
78
100
  python -m pip install --upgrade pip
79
101
  python -m pip install -e ".[test]"
@@ -85,8 +107,12 @@ jobs:
85
107
  run: python -m pytest tests/test_optional_hdf4.py -q
86
108
 
87
109
  test-conda-forge:
88
- name: pytest full-capability (conda-forge)
89
- runs-on: ubuntu-latest
110
+ name: pytest full-capability (conda-forge, ${{ matrix.os }})
111
+ runs-on: ${{ matrix.os }}
112
+ strategy:
113
+ fail-fast: false
114
+ matrix:
115
+ os: [ubuntu-latest, windows-latest]
90
116
  # conda-forge is the *supported* HDF4 install channel (the refusal and
91
117
  # the README point Windows/aarch64 users at `conda install -c
92
118
  # conda-forge ncarnate`). This lane proves that channel actually
@@ -120,12 +146,15 @@ jobs:
120
146
  pyproj
121
147
  tqdm
122
148
  pytest
149
+ pip
123
150
  - name: Install ncarnate from source without replacing native deps
124
151
  # --no-deps: install only ncarnate; the native stack (pyhdf,
125
152
  # netCDF4, numpy, pyproj, tqdm) stays the conda-forge build. Build
126
153
  # isolation still fetches the hatchling backend into a throwaway
127
154
  # env, which never touches the runtime dependencies above.
128
- run: python -m pip install -e . --no-deps
155
+ run: |
156
+ python -m pip install build "hatchling>=1.27"
157
+ python -m pip install -e . --no-deps
129
158
  - name: Prove the conda-forge HDF4 runtime imports
130
159
  run: |
131
160
  python -c "from ncarnate.hdf4_runtime import require_hdf4_runtime; require_hdf4_runtime(); print('conda-forge HDF4 runtime OK')"
@@ -144,14 +173,75 @@ jobs:
144
173
  with:
145
174
  python-version: "3.12"
146
175
  - name: Install build tooling
147
- run: python -m pip install build twine
176
+ run: python -m pip install build twine PyYAML cffconvert
177
+ - name: Validate release identity before building
178
+ env:
179
+ NCARNATE_RELEASE_TAG: ${{ inputs.release_tag || '' }}
180
+ NCARNATE_PUBLISH_TARGET: ${{ inputs.publish_target || '' }}
181
+ run: |
182
+ python tools/validate_release_metadata.py
183
+ cffconvert --validate
148
184
  - name: Build sdist + wheel
149
185
  run: python -m build
150
186
  - name: Check distributions
151
- run: twine check dist/*
187
+ run: twine check --strict dist/*
152
188
  - name: Validate distributions
153
189
  run: >-
154
190
  python tools/validate_distributions.py
155
191
  --allowed-email dev@erickshepherd.com
156
192
  --allowed-email ca@trufo.ai
157
193
  dist/*
194
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
195
+ with:
196
+ name: dist
197
+ path: dist/
198
+ if-no-files-found: error
199
+
200
+ installed-packages:
201
+ name: Installed ${{ matrix.kind }} (${{ matrix.os }})
202
+ needs: build
203
+ runs-on: ${{ matrix.os }}
204
+ timeout-minutes: 20
205
+ strategy:
206
+ fail-fast: false
207
+ matrix:
208
+ os: [ubuntu-latest, windows-latest, macos-latest]
209
+ kind: [wheel, sdist]
210
+ steps:
211
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
212
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
213
+ with:
214
+ python-version: "3.12"
215
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
216
+ with:
217
+ name: dist
218
+ path: dist/
219
+ - name: Test installed package outside the source checkout
220
+ run: python tools/test_distribution.py dist --kind ${{ matrix.kind }}
221
+
222
+ zarr-companion:
223
+ name: Installed Zarr companion
224
+ needs: build
225
+ runs-on: ubuntu-latest
226
+ timeout-minutes: 15
227
+ steps:
228
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
229
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
230
+ with:
231
+ python-version: "3.12"
232
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
233
+ with:
234
+ name: dist
235
+ path: dist/
236
+ - name: Build and install the companion with the candidate wheel
237
+ run: |
238
+ python -m pip install build twine
239
+ python -m build companions/zarr-demo --outdir companion-dist
240
+ python -m twine check --strict companion-dist/*
241
+ python -m pip install dist/*.whl companion-dist/*.whl pytest
242
+ python -m pip check
243
+ - name: Run companion tests outside both source packages
244
+ run: |
245
+ cp -R companions/zarr-demo/tests "$RUNNER_TEMP/zarr-tests"
246
+ cd "$RUNNER_TEMP"
247
+ python -m pytest -q zarr-tests
@@ -0,0 +1,69 @@
1
+ name: Full-granule validation
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ pull_request:
6
+ branches: [main]
7
+ paths:
8
+ - 'ncarnate/**'
9
+ - 'tests/**'
10
+ - 'tools/corpus.py'
11
+ - 'tools/test_full_corpus.py'
12
+ - 'tools/tests/**'
13
+ - '.github/workflows/full-granules.yml'
14
+ - 'pyproject.toml'
15
+ push:
16
+ branches: [main]
17
+ paths:
18
+ - 'ncarnate/**'
19
+ - 'tests/**'
20
+ - 'tools/corpus.py'
21
+ - 'tools/test_full_corpus.py'
22
+ - 'tools/tests/**'
23
+ - '.github/workflows/full-granules.yml'
24
+ - 'pyproject.toml'
25
+
26
+ permissions:
27
+ contents: read
28
+
29
+ jobs:
30
+ full-granules:
31
+ name: Exact originals and independent reference
32
+ runs-on: ubuntu-latest
33
+ timeout-minutes: 30
34
+ steps:
35
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
36
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
37
+ with:
38
+ python-version: "3.12"
39
+ - run: python -m pip install -e ".[test]"
40
+ - name: Set the corpus location outside the checkout
41
+ run: echo "NCARNATE_GRANULE_DIR=$RUNNER_TEMP/ncarnate-granules" >> "$GITHUB_ENV"
42
+ - name: Restore full-size validation inputs
43
+ id: corpus-cache
44
+ uses: actions/cache/restore@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
45
+ with:
46
+ path: ${{ runner.temp }}/ncarnate-granules
47
+ key: corpus-v1-${{ hashFiles('tests/fixtures/corpus.json') }}
48
+ # Cache hits must pass the same digest verification as fresh downloads.
49
+ - name: Fetch and verify every required file before running tests
50
+ run: >-
51
+ python tools/corpus.py "$NCARNATE_GRANULE_DIR" --fetch --include-references
52
+ > corpus-verification.json
53
+ - name: Cache only a fully verified corpus
54
+ if: steps.corpus-cache.outputs.cache-hit != 'true'
55
+ uses: actions/cache/save@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3
56
+ with:
57
+ path: ${{ runner.temp }}/ncarnate-granules
58
+ key: ${{ steps.corpus-cache.outputs.cache-primary-key }}
59
+ - name: Run full-size conversions and reference comparisons
60
+ run: python tools/test_full_corpus.py --junitxml full-granules.xml
61
+ - name: Retain verification and test results, including failures
62
+ if: always()
63
+ uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
64
+ with:
65
+ name: full-granule-results
66
+ path: |
67
+ corpus-verification.json
68
+ full-granules.xml
69
+ if-no-files-found: warn
@@ -0,0 +1,120 @@
1
+ name: Publish to PyPI
2
+
3
+ # Publishes the package via PyPI Trusted Publishing (OIDC) — no API token is
4
+ # stored anywhere (not in the repo, not in GitHub secrets). PyPI verifies the
5
+ # workflow's short-lived OIDC identity instead.
6
+ #
7
+ # Two ways to run:
8
+ # * A published GitHub Release -> builds and publishes to PyPI.
9
+ # * Manual "Run workflow" -> choose testpypi (dry run) or pypi.
10
+ #
11
+ # The publish jobs run in protected GitHub Environments (`pypi` / `testpypi`).
12
+ # Add yourself as a required reviewer on each (repo Settings -> Environments)
13
+ # so every upload pauses for your one-click approval before it leaves.
14
+
15
+ on:
16
+ release:
17
+ types: [released] # full releases only — pre-releases never auto-publish
18
+ workflow_dispatch:
19
+ inputs:
20
+ target:
21
+ description: "Where to publish"
22
+ type: choice
23
+ options: [testpypi, pypi]
24
+ default: testpypi
25
+ recover_230:
26
+ description: "Retry only the verified 2.3.0 artifacts after the uploader failure"
27
+ type: boolean
28
+ default: false
29
+
30
+ permissions:
31
+ contents: read
32
+
33
+ jobs:
34
+ verify:
35
+ name: Test and build the exact release revision
36
+ if: ${{ !inputs.recover_230 }}
37
+ # A local reusable workflow uses the same revision as this caller.
38
+ # Its dist artifact is installed by the package tests and then published
39
+ # unchanged below. Failure of ANY CI job prevents either upload.
40
+ uses: ./.github/workflows/ci.yml
41
+ with:
42
+ release_tag: ${{ github.event_name == 'release' && github.event.release.tag_name || (github.ref_type == 'tag' && github.ref_name || '') }}
43
+ publish_target: ${{ github.event_name == 'release' && 'pypi' || inputs.target }}
44
+
45
+ publish-testpypi:
46
+ name: Publish to TestPyPI
47
+ needs: verify
48
+ if: github.event_name == 'workflow_dispatch' && inputs.target == 'testpypi'
49
+ runs-on: ubuntu-latest
50
+ environment: testpypi
51
+ permissions:
52
+ id-token: write
53
+ steps:
54
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
55
+ with:
56
+ name: dist
57
+ path: dist/
58
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
59
+ with:
60
+ repository-url: https://test.pypi.org/legacy/
61
+ # Duplicate versions fail: success must mean these files were uploaded.
62
+
63
+ verify-recovery:
64
+ name: Verify the preserved 2.3.0 release artifacts
65
+ if: github.event_name == 'workflow_dispatch' && inputs.recover_230 && inputs.target == 'pypi' && github.ref == 'refs/heads/main'
66
+ runs-on: ubuntu-latest
67
+ permissions:
68
+ contents: read
69
+ actions: read
70
+ steps:
71
+ - uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09
72
+ - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1
73
+ with:
74
+ python-version: "3.12"
75
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
76
+ with:
77
+ name: dist
78
+ path: dist/
79
+ run-id: 37859119484
80
+ github-token: ${{ github.token }}
81
+ - name: Verify original run, all release tests, tag and exact package bytes
82
+ env:
83
+ GH_TOKEN: ${{ github.token }}
84
+ run: python tools/verify_release_retry.py dist
85
+ - run: python -m pip install "twine>=7,<8"
86
+ - run: python -m twine check --strict dist/*
87
+ - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
88
+ with:
89
+ name: recovery-dist
90
+ path: dist/
91
+ if-no-files-found: error
92
+
93
+ publish-recovery:
94
+ name: Retry the verified 2.3.0 upload
95
+ needs: verify-recovery
96
+ runs-on: ubuntu-latest
97
+ environment: pypi
98
+ permissions:
99
+ id-token: write
100
+ steps:
101
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
102
+ with:
103
+ name: recovery-dist
104
+ path: dist/
105
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
106
+
107
+ publish-pypi:
108
+ name: Publish to PyPI
109
+ needs: verify
110
+ if: github.event_name == 'release' || (github.event_name == 'workflow_dispatch' && inputs.target == 'pypi')
111
+ runs-on: ubuntu-latest
112
+ environment: pypi
113
+ permissions:
114
+ id-token: write
115
+ steps:
116
+ - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0
117
+ with:
118
+ name: dist
119
+ path: dist/
120
+ - uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -23,3 +23,9 @@ docs/audits/*.audit-state.json
23
23
 
24
24
  # Sphinx documentation build output.
25
25
  docs/_build/
26
+
27
+ # Machine-local publication policy, requests and private reports.
28
+ .publication-private/
29
+ .publication-reports/
30
+ .publication-cache/
31
+ full-granules.xml
@@ -3,7 +3,7 @@
3
3
  version: 2
4
4
 
5
5
  build:
6
- os: ubuntu-22.04
6
+ os: ubuntu-24.04
7
7
  tools:
8
8
  python: "3.12"
9
9
 
@@ -0,0 +1,26 @@
1
+ {
2
+ "upload_type": "software",
3
+ "version": "2.3.1",
4
+ "title": "ncarnate: Convert HDF4/HDF-EOS2 (MODIS, AMSR-E) to CF-annotated netCDF4 and losslessly recompress netCDF/HDF5",
5
+ "description": "A Python library and command-line tool for converting HDF4/HDF-EOS2 granules to CF-annotated netCDF4 and losslessly recompressing netCDF/HDF5. Version 2.3.1 adds convert_file(), a one-file Python API that requires a separate output and explicit permission to replace existing output files, while preserving the legacy recompress() API. Supports prepared jobs, digest-verified resume, streamed copying and verification, and optional isolated conversion workers with OS-enforced memory limits on Windows and Linux. Outputs are verified value-for-value before publication. Generated coordinate fields can still require whole arrays, and the worker limit excludes the calling process.",
6
+ "access_right": "open",
7
+ "license": "MIT",
8
+ "keywords": [
9
+ "netCDF4",
10
+ "HDF4",
11
+ "HDF-EOS2",
12
+ "HDF5",
13
+ "CF conventions",
14
+ "MODIS",
15
+ "AMSR-E",
16
+ "remote sensing",
17
+ "atmospheric science",
18
+ "Python"
19
+ ],
20
+ "creators": [
21
+ {
22
+ "name": "Shepherd, Erick Edward",
23
+ "orcid": "0000-0002-4750-6100"
24
+ }
25
+ ]
26
+ }
@@ -4,6 +4,65 @@ All notable changes to this project are documented here. The format is
4
4
  based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
5
5
  this project adheres to [Semantic Versioning](https://semver.org/).
6
6
 
7
+ ## Unreleased
8
+
9
+ ## [2.3.1] - 2026-10-09
10
+
11
+ - Add `convert_file(src, dst, ...)` as the preferred one-file Python API for
12
+ conversion and recompression. It requires a separate output, preserves the
13
+ source, and refuses existing outputs unless `overwrite=True` is supplied.
14
+ - Verify the output before publication; default publication also refuses an
15
+ output created while conversion was running. This mode requires a filesystem
16
+ with hard-link support and fails safely when that support is unavailable.
17
+ - Keep `recompress()`, `convert_manifest()`, the `ncarnate.convert` module and
18
+ all command-line behavior compatible with 2.3.0.
19
+ - Restore netCDF3 input conversion in the Python APIs, CLI and manifest runs
20
+ after the 2.3.0 chunk-cache regression attempted an HDF5-only operation on
21
+ classic files.
22
+
23
+ ## [2.3.0] - 2026-10-08
24
+
25
+ - Add explicit-output prepared plans, shared finite-batch collision checks and
26
+ digest-verified resume journals. Existing one-shot defaults remain compatible.
27
+ - Store completion checkpoints separately so large batches remain resumable;
28
+ resolve directory aliases and check every output parent before execution.
29
+ - Refuse unknown inputs before output-directory creation; reserve CLI result
30
+ journals before conversion and distinguish later reporting failures.
31
+ - Bound hostile handoff records, refuse invalid text and audit modes, freeze
32
+ encoding options, and guard unsynchronized attribute inquiries.
33
+ - Stream netCDF and HDF4 SDS copying and verification in slices; offer an
34
+ OS-enforced conversion-worker memory cap on Windows and Linux. Generated
35
+ HDF-EOS coordinates still materialize whole arrays within that worker cap,
36
+ while tiled swath interpolation avoids large full-grid temporaries.
37
+ - Preserve reads of multidimensional variables named like dimensions when
38
+ configuring netCDF native caches (found with the complete MOD03 granule).
39
+ - Finish large storage chunks before advancing and coalesce small chunks to
40
+ avoid repeated compression and excessive per-chunk Python calls.
41
+ - Add exact-granule retrieval and verification, Windows test portability fixes,
42
+ and a separately packaged numeric Zarr/xarray demonstration.
43
+ - Document the public staged, prepared, handoff and bounded-worker APIs on Read
44
+ the Docs, and synchronize package, citation, Zenodo and conda recipe metadata.
45
+ - Gate publication on the complete CI suite, test built packages outside the
46
+ checkout on Linux, Windows and macOS, and test the separate Zarr companion.
47
+ Add release-PR and manual full-granule validation that refuses incomplete evidence; require
48
+ matching production tags and fail duplicate TestPyPI uploads.
49
+ - Reconcile MYD05 fixture regeneration with its recorded trimmed metadata and
50
+ emit canonical source-archive gzip headers for publication inspection.
51
+
52
+ ### Compatibility notes
53
+
54
+ - Existing one-shot source-replacement defaults remain unchanged. The new
55
+ prepared and bounded APIs always require a separate explicit output.
56
+ - Encoding options are now immutable, enforcing the documented immutable-plan
57
+ contract. Use `dataclasses.replace(options, complevel=...)` to derive options;
58
+ assigning to a field now raises `FrozenInstanceError`.
59
+ - Unknown audit modes and invalid UTF-8 result text are now deliberately refused.
60
+ Metadata-only audit is the only implemented audit depth.
61
+ - The handoff schema stays at version 1. New diagnostic codes are append-only;
62
+ the classification ruleset advances independently to version 9.
63
+ - Hard worker limits are available on Windows and Linux; macOS is explicitly
64
+ unsupported by `execute_bounded`. Ordinary conversion remains portable.
65
+
7
66
  ## [2.2.2] - 2026-07-21
8
67
 
9
68
  Maintenance release with packaging and CI updates. Conversion, recompression,
@@ -5,14 +5,17 @@ abstract: >-
5
5
  Reincarnate legacy scientific data as modern netCDF4: losslessly
6
6
  recompress netCDF/HDF5 files and convert HDF4/HDF-EOS2 granules to
7
7
  CF-annotated netCDF4 with reconstructed grid and swath geolocation.
8
+ Supports verified resumable conversions, streamed data transfer, and
9
+ isolated conversion workers with memory limits on Windows and Linux.
8
10
  type: software
9
11
  authors:
10
12
  - family-names: "Shepherd"
11
13
  given-names: "Erick Edward"
12
14
  email: "dev@erickshepherd.com"
13
15
  orcid: "https://orcid.org/0000-0002-4750-6100"
14
- version: "2.2.2"
15
- date-released: "2026-07-21"
16
+ version: "2.3.1"
17
+ date-released: 2026-10-09
18
+ # This is the verified concept DOI, which identifies all versions.
16
19
  doi: "10.5281/zenodo.21288802"
17
20
  license: MIT
18
21
  repository-code: "https://github.com/ErickShepherd/ncarnate"
@@ -0,0 +1,98 @@
1
+ # Contributing to ncarnate
2
+
3
+ Thanks for your interest in ncarnate. Contributions — bug reports, fixes,
4
+ documentation, and new format support — are welcome.
5
+
6
+ ## Reporting issues
7
+
8
+ Please open an issue at
9
+ <https://github.com/ErickShepherd/ncarnate/issues>. A good report includes:
10
+
11
+ - what you ran (the exact command or code) and what happened,
12
+ - the input file's format (netCDF3/4, HDF5, HDF4/HDF-EOS2) and, if possible, a
13
+ small sample or its structure (`ncdump -h`, or the HDF-EOS `StructMetadata`),
14
+ - your OS, Python version, and the installed versions of `ncarnate`, `netCDF4`,
15
+ and `pyhdf`.
16
+
17
+ Because ncarnate makes a **fidelity guarantee** (see `docs/fidelity-notes.md`),
18
+ any case where a converted or recompressed file does *not* re-read identically
19
+ to its source is treated as a correctness bug — please report it.
20
+
21
+ ## Seeking support
22
+
23
+ For usage questions, open a
24
+ [Discussion](https://github.com/ErickShepherd/ncarnate/discussions) if enabled,
25
+ or an issue labelled `question`. The README covers installation (including the
26
+ `pyhdf`-on-Windows caveat), CLI and library usage, and the supported inputs.
27
+
28
+ ## Development setup
29
+
30
+ ```console
31
+ git clone https://github.com/ErickShepherd/ncarnate
32
+ cd ncarnate
33
+ python -m pip install -e ".[test,release]" "ruff==0.15.*"
34
+ python -m pip install -r docs/requirements.txt
35
+ ```
36
+
37
+ Compatible pip wheels can supply the native dependencies. If a wheel is not
38
+ available for your platform and Python version, use conda-forge or install the
39
+ native libraries and build tools described in the README. Ordinary conversion
40
+ and the hard memory-limit worker have different platform coverage: the worker
41
+ supports Windows and Linux and deliberately refuses macOS.
42
+
43
+ ## Making a change
44
+
45
+ 1. Fork the repository and create a topic branch off `main`.
46
+ 2. Make your change with a focused commit history.
47
+ 3. **Add or update tests.** The suite runs entirely offline against small
48
+ committed fixtures trimmed from real granules; a new format or fix should
49
+ come with a fixture-backed test that pins the behaviour.
50
+ 4. Run the checks locally:
51
+ ```console
52
+ ruff check .
53
+ python -m pytest -q
54
+ python -m pytest -q tools/tests
55
+ python tools/validate_release_metadata.py
56
+ cffconvert --validate
57
+ python -m sphinx -W --keep-going -b html docs docs/_build/html
58
+ ```
59
+ 5. Open a pull request describing the change and, for a conversion change, how
60
+ it preserves the fidelity contract (stored values unchanged; output verified
61
+ against the source before it replaces anything).
62
+
63
+ For full-size regression evidence, fetch and verify the catalogued originals
64
+ and independent reference with
65
+ `python tools/corpus.py DATA_DIRECTORY --fetch --include-references`.
66
+ Set `NCARNATE_GRANULE_DIR` to that directory and
67
+ run `python tools/test_full_corpus.py --junitxml full-granules.xml`. That check
68
+ fails on missing files or skipped/missing tests; ordinary pytest remains usable
69
+ without the external corpus. Keep downloaded inputs and reports outside the
70
+ tracked source tree. The full-granule GitHub workflow retains its reports and
71
+ uses a cache keyed by the corpus manifest. It verifies every cached file before
72
+ testing. Downloads retry temporary failures at most three times per URL, with
73
+ 2- and 4-second pauses; errors appear in the job log even when JSON is redirected.
74
+ Bad checksums and permanent HTTP errors are not retried. See
75
+ [CI dependencies](docs/ci-dependencies.rst) for data sources and the mirror plan.
76
+
77
+ The Zarr demonstration requires Python 3.11 or newer and is a separate package:
78
+ install `companions/zarr-demo` after the candidate ncarnate package, then run
79
+ `python -m pytest -q companions/zarr-demo/tests`. Its CI job builds and tests the
80
+ installed wheel outside both source packages.
81
+
82
+ To exercise release installations locally, build to a directory outside the
83
+ checkout with `python -m build --outdir PATH_TO_ARTIFACTS`, then run
84
+ `python tools/test_distribution.py PATH_TO_ARTIFACTS --kind wheel` and repeat
85
+ with `--kind sdist`. Each run creates a clean environment and tests the installed
86
+ package. PyPI and TestPyPI wait for these jobs and the full source CI. Publication
87
+ is a separate maintainer action; see `docs/releasing.rst`.
88
+
89
+ ## Scope and design
90
+
91
+ ncarnate deliberately **fails loud** on constructs it cannot convert correctly
92
+ rather than guessing — a wrong coordinate is worse than a refused conversion.
93
+ New support should extend that contract, not weaken it: prefer a clear,
94
+ tested error over a silent approximation. When in doubt, open an issue to
95
+ discuss the approach before a large change.
96
+
97
+ By contributing, you agree that your contributions are licensed under the
98
+ project's [MIT License](LICENSE).