diff --git a/bridge/uv.lock b/bridge/uv.lock index ea2fe4f..4ec4c37 100644 --- a/bridge/uv.lock +++ b/bridge/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "coolscanpy" -version = "0.7.10" +version = "0.7.11" source = { editable = "../coolscanpy" } dependencies = [ { name = "imagecodecs" }, diff --git a/coolscanpy/pyproject.toml b/coolscanpy/pyproject.toml index 9856aaf..bf944f9 100644 --- a/coolscanpy/pyproject.toml +++ b/coolscanpy/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "coolscanpy" -version = "0.7.10" +version = "0.7.11" description = "Direct-USB acquisition library for Nikon Coolscan film scanners with a python-sane-style API — roll scanning, RGBI capture, IR plane, scan receipts" readme = "README.md" license = "GPL-3.0-only" diff --git a/coolscanpy/src/coolscanpy/_roll.py b/coolscanpy/src/coolscanpy/_roll.py index 31b5866..a28f0fa 100644 --- a/coolscanpy/src/coolscanpy/_roll.py +++ b/coolscanpy/src/coolscanpy/_roll.py @@ -51,6 +51,7 @@ SinglePassFinalizationResult, SinglePassSession, SinglePassWorkflowError, + _exposure_authority_is_bound, ) from coolscanpy.exceptions import ( AdapterUnsupported, @@ -2651,24 +2652,13 @@ def _read_exact_analyzer_source( raise BatchIntegrityError( "settled third meter pass is not bound to the accepted controller result" ) - # Since the guarded nikon-parity solve became the RGB command authority, - # the commanded contract is bound to the active controller's accepted - # solve THROUGH the journaled authority record: active solve -> authority - # -> commanded contract, with infrared passing through unchanged. - authority = journal.get("active_exposure_authority") - if ( - type(authority) is not dict - or authority.get("rgb_source") != "nikon-parity-guarded-v2" - or authority.get("ir_source") != "active-controller" - or authority.get("commanded_channels_raw_10ns") != final_controller - or authority.get("active_controller_channels_raw_10ns") - != controller.get("final_exposures_raw_10ns") - or type(controller.get("final_exposures_raw_10ns")) is not dict - or final_controller.get("IR") - != controller["final_exposures_raw_10ns"].get("IR") + if not _exposure_authority_is_bound( + journal, + final_controller, + controller.get("final_exposures_raw_10ns"), ): raise BatchIntegrityError( - "commanded exposure contract is not bound to the parity authority " + "commanded exposure contract is not bound to the declared authority " "and the accepted controller result" ) diff --git a/coolscanpy/src/coolscanpy/capture/single_pass_workflow.py b/coolscanpy/src/coolscanpy/capture/single_pass_workflow.py index 26a60ac..a8481ef 100644 --- a/coolscanpy/src/coolscanpy/capture/single_pass_workflow.py +++ b/coolscanpy/src/coolscanpy/capture/single_pass_workflow.py @@ -60,6 +60,7 @@ decode_full_records, decode_records, ) +from coolscanpy.protocol.ls5000_single_pass.meter import EXPOSURE_MAX, EXPOSURE_MIN from coolscanpy.protocol.ls5000_single_pass.streaming_sidecar import ( CANONICAL_RGB_AVERAGE, STREAM_DATA_SUFFIX, @@ -121,6 +122,48 @@ ) +def _exposure_authority_is_bound( + journal: Mapping[str, Any], + commanded: Mapping[str, int], + active_solve: object, +) -> bool: + authority = journal.get("active_exposure_authority") + if ( + type(authority) is not dict + or authority.get("ir_source") != "active-controller" + or authority.get("commanded_channels_raw_10ns") != commanded + or type(active_solve) is not dict + or authority.get("active_controller_channels_raw_10ns") != active_solve + or commanded.get("IR") != active_solve.get("IR") + ): + return False + source = authority.get("rgb_source") + if source == "nikon-parity-guarded-v2": + return True + if source != "explicit-rgb-override": + return False + override = journal.get("exposure_override") + forced = override.get("forced_10ns") if type(override) is dict else None + if ( + type(override) is not dict + or override.get("applied") is not True + or type(forced) is not dict + or set(forced) != {"red", "green", "blue"} + or authority.get("device_exposure_bounds_raw_10ns") + != [EXPOSURE_MIN, EXPOSURE_MAX] + ): + return False + for name, channel in (("red", "R"), ("green", "G"), ("blue", "B")): + value = forced[name] + if ( + type(value) is not int + or not EXPOSURE_MIN <= value <= EXPOSURE_MAX + or value != commanded.get(channel) + ): + return False + return True + + class SinglePassWorkflowError(RuntimeError): """A capture cannot safely become a committed NegPy artifact set.""" @@ -1476,24 +1519,15 @@ def _validate_completed_capture( } if controller_exposures != expected_controller: raise SinglePassIntegrityError("meter controller exposure echo is inconsistent") - # Guarded nikon-parity is the RGB command authority: the commanded - # contract is bound to the active controller's accepted solve THROUGH - # the journaled authority record (active solve -> authority -> - # commanded wire echo), with infrared passing through unchanged. - # Mirrors the roll publication consumer's binding check. authority = journal.get("active_exposure_authority") active_solve = controller.get("final_exposures_raw_10ns") - if ( - not isinstance(authority, dict) - or authority.get("rgb_source") != "nikon-parity-guarded-v2" - or authority.get("ir_source") != "active-controller" - or authority.get("commanded_channels_raw_10ns") != expected_controller - or not isinstance(active_solve, dict) - or authority.get("active_controller_channels_raw_10ns") != active_solve - or expected_controller.get("IR") != active_solve.get("IR") + if not _exposure_authority_is_bound( + journal, + expected_controller, + active_solve, ): raise SinglePassIntegrityError( - "commanded exposure contract is not bound to the parity authority " + "commanded exposure contract is not bound to the declared authority " "and the accepted controller result" ) diff --git a/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/bundle.py b/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/bundle.py index 71ddbfe..41e6269 100644 --- a/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/bundle.py +++ b/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/bundle.py @@ -28,6 +28,7 @@ # density-source cap-0x10d/f03 exposures, the proven 97-dpi reservation-preview # evidence, runtime arithmetic gate, and exact per-frame ownership receipt. CAPTURE_BUNDLE_COMPONENT_SHA256 = { + # Resealed 2026-09-08: resumed batches validate the original capture plan. # Resealed 2026-09-08: held-child non-motion film-status rendezvous. # 0.7.9 preview: held metering/explicit refusal skips plus retained local # timing, required scanner identity and adapter-conditional replay. @@ -63,7 +64,7 @@ # Resealed 2026-08-11: capture descendants remain in the bridge-owned # process group and inherit its exclusive process-ownership fence. "capture_process.py": "e66a50d07a4704a10f38f3d477432e0e2b3853c23c43a70e740950bd2dccb2ea", - "worker.py": "41588f69eb75ac67c2f8a2bb86f8bf175ae9949109c06ca9256115a7362ecd41", + "worker.py": "e8bd899f166042ca7d6cc2678df7f585a47d2e7ac38d131f3c65f9ffdeec502c", "manual_frames.py": "8fc4ba82c177e1b7ecd6943354db33930b468ef3b4b82c712202b8caea54c9bb", "usb_backend.py": "afb5b3cbb57404b758f4f8d8795f4307c07c8f6d01bbeccb3ced38026787fd62", "density.py": "c2c47de2886bc4b60197d2721b6d72050a76f1095760590fa7bb34a728b9da76", diff --git a/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/worker.py b/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/worker.py index c0314f9..afa7cce 100644 --- a/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/worker.py +++ b/coolscanpy/src/coolscanpy/protocol/ls5000_single_pass/worker.py @@ -8148,7 +8148,7 @@ def run_live_capture( derive_equivalent_continuation_blocks(continuation_plan) batch_job = next_batch_job batch_selections = _derive_live_batch_selections( - active_plan, + plan, preview_bytes, live_sub_8e_table, batch_job.frames, diff --git a/coolscanpy/tests/capture/test_ls5000_single_pass_workflow.py b/coolscanpy/tests/capture/test_ls5000_single_pass_workflow.py index 44e3067..c36fe2d 100644 --- a/coolscanpy/tests/capture/test_ls5000_single_pass_workflow.py +++ b/coolscanpy/tests/capture/test_ls5000_single_pass_workflow.py @@ -888,25 +888,25 @@ def assessor(rgb: np.ndarray, *, dpi: int) -> StoppedTransportSmearAssessment: ), ( lambda journal: journal.__delitem__("active_exposure_authority"), - "parity authority", + "declared authority", ), ( lambda journal: journal["active_exposure_authority"][ "commanded_channels_raw_10ns" ].__setitem__("G", 1), - "parity authority", + "declared authority", ), ( lambda journal: journal["active_exposure_authority"][ "active_controller_channels_raw_10ns" ].__setitem__("R", 1), - "parity authority", + "declared authority", ), ( lambda journal: journal["meter_controller_final_result"][ "final_exposures_raw_10ns" ].__setitem__("IR", 1), - "parity authority", + "declared authority", ), ], ) @@ -973,6 +973,67 @@ def test_parity_commanded_exposures_differing_from_active_solve_finalize( assert authority["active_controller_channels_raw_10ns"] == active +def _record_explicit_override(journal: dict[str, Any]) -> None: + commanded = journal["active_exposure_authority"][ + "commanded_channels_raw_10ns" + ] + journal["active_exposure_authority"]["rgb_source"] = "explicit-rgb-override" + journal["exposure_override"] = { + "applied": True, + "forced_10ns": { + "red": commanded["R"], + "green": commanded["G"], + "blue": commanded["B"], + }, + "metered_10ns": { + "red": commanded["R"], + "green": commanded["G"], + "blue": commanded["B"], + }, + } + + +def test_explicit_override_authority_finalizes_when_exactly_bound( + tmp_path: Path, +) -> None: + attempt, _stream = _attempt(tmp_path) + journal = json.loads(attempt.journal_path.read_text(encoding="utf-8")) + _record_explicit_override(journal) + attempt.journal_path.write_text(json.dumps(journal), encoding="utf-8") + + result = _workflow().finalize_attempt(attempt, delete_scratch=False) + + authority = result.manifest["exposure_evidence"]["active_exposure_authority"] + assert authority["rgb_source"] == "explicit-rgb-override" + + +def test_explicit_override_authority_refuses_tampered_forced_ticks( + tmp_path: Path, +) -> None: + attempt, stream = _attempt(tmp_path) + journal = json.loads(attempt.journal_path.read_text(encoding="utf-8")) + _record_explicit_override(journal) + journal["exposure_override"]["forced_10ns"]["red"] += 1 + attempt.journal_path.write_text(json.dumps(journal), encoding="utf-8") + + with pytest.raises(SinglePassIntegrityError, match="authority"): + _workflow().finalize_attempt(attempt) + + assert stream.is_file() + + +def test_legacy_parity_labeled_override_still_finalizes(tmp_path: Path) -> None: + attempt, _stream = _attempt(tmp_path) + journal = json.loads(attempt.journal_path.read_text(encoding="utf-8")) + _record_explicit_override(journal) + journal["active_exposure_authority"]["rgb_source"] = ( + "nikon-parity-guarded-v2" + ) + attempt.journal_path.write_text(json.dumps(journal), encoding="utf-8") + + _workflow().finalize_attempt(attempt, delete_scratch=False) + + def test_stream_hash_mismatch_retains_scratch_before_decode(tmp_path: Path) -> None: attempt, stream = _attempt(tmp_path) stream.write_bytes(b"87654321") diff --git a/coolscanpy/tests/protocol/ls5000_single_pass/test_worker.py b/coolscanpy/tests/protocol/ls5000_single_pass/test_worker.py index a4ec198..42cbf20 100644 --- a/coolscanpy/tests/protocol/ls5000_single_pass/test_worker.py +++ b/coolscanpy/tests/protocol/ls5000_single_pass/test_worker.py @@ -7062,7 +7062,7 @@ def dispose_resources(_device: object) -> None: pass def derive_batch( - _plan: list[dict], + selection_plan: list[dict], preview: bytes, table: bytes, frames: tuple, @@ -7070,6 +7070,7 @@ def derive_batch( reviewed_fingerprint: ReviewedRollFingerprint, manual_boundary_rows: tuple[int, ...] | None = None, ) -> tuple: + worker_module.validate_plan(selection_plan) assert len(preview) == len(worker_module.PREVIEW_READ_SEQUENCES) assert table == header_8e assert len(frames) == 1 diff --git a/coolscanpy/tests/protocol/ls5000_single_pass/test_worker_streaming_integration.py b/coolscanpy/tests/protocol/ls5000_single_pass/test_worker_streaming_integration.py index ef4b12b..f21e6f8 100644 --- a/coolscanpy/tests/protocol/ls5000_single_pass/test_worker_streaming_integration.py +++ b/coolscanpy/tests/protocol/ls5000_single_pass/test_worker_streaming_integration.py @@ -403,15 +403,8 @@ def test_continuation_loop_wires_hook_and_binds_finish_to_raw_sha( assert log["finish_calls"] == [(0, hashlib.sha256(b"x").hexdigest(), 1)] -def test_continuation_meter_evidence_is_accepted_by_publication_consumer( - tmp_path: Path, monkeypatch: pytest.MonkeyPatch -) -> None: - journal, second = _drive_continuation( - tmp_path, - monkeypatch, - open_session=lambda *_a: None, - full_meter_payload=True, - ) +def _publication_consumer_inputs(tmp_path: Path, journal, second): + second.journal.write_text(json.dumps(journal), encoding="utf-8") journal_payload = second.journal.read_bytes() attempt = SinglePassAttempt( session=SinglePassSession(root=tmp_path, session_id="continuation-consumer"), @@ -441,6 +434,19 @@ def test_continuation_meter_evidence_is_accepted_by_publication_consumer( resumed=False, scratch_deleted=False, ) + return attempt, finalization + + +def test_continuation_meter_evidence_is_accepted_by_publication_consumer( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + journal, second = _drive_continuation( + tmp_path, + monkeypatch, + open_session=lambda *_a: None, + full_meter_payload=True, + ) + attempt, finalization = _publication_consumer_inputs(tmp_path, journal, second) meter_rgbi, final_rgb = roll_module._read_exact_analyzer_source( attempt, finalization @@ -460,6 +466,63 @@ def test_continuation_meter_evidence_is_accepted_by_publication_consumer( ) +def test_publication_consumer_accepts_bound_explicit_override( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + journal, second = _drive_continuation( + tmp_path, + monkeypatch, + open_session=lambda *_a: None, + full_meter_payload=True, + ) + commanded = journal["active_exposure_authority"][ + "commanded_channels_raw_10ns" + ] + journal["active_exposure_authority"]["rgb_source"] = "explicit-rgb-override" + journal["exposure_override"] = { + "applied": True, + "forced_10ns": { + "red": commanded["R"], + "green": commanded["G"], + "blue": commanded["B"], + }, + } + attempt, finalization = _publication_consumer_inputs(tmp_path, journal, second) + + _meter_rgbi, final_rgb = roll_module._read_exact_analyzer_source( + attempt, finalization + ) + + assert final_rgb == (commanded["R"], commanded["G"], commanded["B"]) + + +def test_publication_consumer_refuses_tampered_explicit_override( + tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + journal, second = _drive_continuation( + tmp_path, + monkeypatch, + open_session=lambda *_a: None, + full_meter_payload=True, + ) + commanded = journal["active_exposure_authority"][ + "commanded_channels_raw_10ns" + ] + journal["active_exposure_authority"]["rgb_source"] = "explicit-rgb-override" + journal["exposure_override"] = { + "applied": True, + "forced_10ns": { + "red": commanded["R"] + 1, + "green": commanded["G"], + "blue": commanded["B"], + }, + } + attempt, finalization = _publication_consumer_inputs(tmp_path, journal, second) + + with pytest.raises(roll_module.BatchIntegrityError, match="authority"): + roll_module._read_exact_analyzer_source(attempt, finalization) + + def test_synchronous_decoder_exception_never_aborts_continuation_capture( tmp_path: Path, monkeypatch: pytest.MonkeyPatch ) -> None: diff --git a/coolscanpy/uv.lock b/coolscanpy/uv.lock index d297e79..26f920c 100644 --- a/coolscanpy/uv.lock +++ b/coolscanpy/uv.lock @@ -13,7 +13,7 @@ wheels = [ [[package]] name = "coolscanpy" -version = "0.7.10" +version = "0.7.11" source = { editable = "." } dependencies = [ { name = "imagecodecs" }, diff --git a/docs/releases/v0.7.0-beta.19.md b/docs/releases/v0.7.0-beta.19.md new file mode 100644 index 0000000..bb50252 --- /dev/null +++ b/docs/releases/v0.7.0-beta.19.md @@ -0,0 +1,54 @@ +# ScanStudio v0.7.0-beta.19 + +This patch fixes locked RGB exposure capture and subsequent 1× captures in a +held scanner session, including CLI repeated passes. + +## Everything that changed + +- Accept a correctly recorded explicit RGB exposure override after capture. + Beta.18 could read a complete image and then reject its exposure source + label instead of producing a receipt. Both capture consumers now check the + recorded override against the commanded RGB values and device bounds. + Accepted controller evidence and unchanged metered infrared remain required. +- Validate the original scan plan when starting another held capture round. + Previously, a first 1× capture modified the active plan, and the next round + incorrectly checked that modified plan against the original 4× contract. + Each round still applies and checks its requested sampling mode separately. +- Bundle CoolscanPy 0.7.11 so standalone driver users receive the same fixes. + +## Validation + +Focused regressions cover accepted and tampered exposure overrides, legacy +exposure evidence, and two-round 1×→1×, 4×→4× and 1×→4× capture sequences. +The existing two-round test now validates its input plan, which reproduces +the hardware failure before the fix. Signed local packaging, relocation and +isolated bridge checks passed. + +A signed local candidate completed two CLI captures of frame 2 on one real +LS-5000 ED/SA-30 at 4000 dpi, 16-bit RGBI and 1× sampling. Both jobs completed +with one receipt each. CLI receipt verification, exposure consistency and +the no-clipping check passed with no issues. Independent journal comparison +confirmed identical locked RGB values across both passes, independently +metered IR and distinct capture directories. The host stopped without an +eject command. Original capture files, journals and receipts are preserved. + +## Platform support and installation + +Apple Silicon Macs running macOS 14 (Sonoma) or newer are supported. Download +`ScanStudio-0.7.0-beta.19-macOS-arm64.dmg` from this prerelease. The release +workflow signs, notarizes and staples the app and DMG before publication. +Intel Macs, Windows and Linux are not supported ScanStudio platforms. + +The bundled CLI is `ScanStudio.app/Contents/MacOS/scanstudio-cli`. See +[the CLI guide](../CLI.md) and [hardware support](../HARDWARE-SUPPORT.md). + +## Known limitations + +These fixes do not qualify other scanners or establish color, clipping or +calibration accuracy. The physical calibration campaign and full-roll +preview-derived exposure acceptance remain incomplete. Preview-derived +exposure remains off by default. LS-50 still requires the existing +unverified-hardware opt-in and has not been physically tested here. + +Stop after a failed motion and preserve its diagnostics before another +attempt. No original evidence is rewritten to make verification pass. diff --git a/scripts/check_coolscanpy_pypi_sync.sh b/scripts/check_coolscanpy_pypi_sync.sh index 39e06a1..a1a5ebd 100755 --- a/scripts/check_coolscanpy_pypi_sync.sh +++ b/scripts/check_coolscanpy_pypi_sync.sh @@ -6,7 +6,7 @@ # debugging confusion the instrumented-refusal work exists to prevent # (owner policy, 2026-08-08: "keep them in sync so there's no delta"). # -# Mechanics: download the exact authenticated coolscanpy 0.7.10 sdist and compare its +# Mechanics: download the exact authenticated coolscanpy 0.7.11 sdist and compare its # src/coolscanpy tree byte-for-byte against this repo's vendored # coolscanpy/src/coolscanpy. Version strings are NOT trusted as the primary # signal (an unbumped version with changed code is precisely the failure @@ -30,33 +30,33 @@ VENDORED_DIR="coolscanpy/src/coolscanpy" # Re-pin: shasum -a 256 , update the entry in the same change # that alters the file. KNOWN_VENDORED_DIVERGENCE=( - # required scanner_identity + capture-timing feature; re-pinned for 0.7.10 + # required scanner_identity + capture-timing feature; re-pinned for 0.7.11 # (held metering and exact-slot refusal continuation landed upstream) - "protocol/ls5000_single_pass/worker.py|41588f69eb75ac67c2f8a2bb86f8bf175ae9949109c06ca9256115a7362ecd41|5ae9ea9d0cc450fd4bc84322f0417c16d2dc1486f4f8a84f057494949f5b5faf" - # LeadingFrameClippedError + confident-clear-film gate; re-pinned for 0.7.10 + "protocol/ls5000_single_pass/worker.py|e8bd899f166042ca7d6cc2678df7f585a47d2e7ac38d131f3c65f9ffdeec502c|d638aaff349895352a086343581b86cfc1bff3d266d23ef9e8104c8b41876bd5" + # LeadingFrameClippedError + confident-clear-film gate; re-pinned for 0.7.11 # (held metering and exact-slot refusal continuation landed upstream) "protocol/ls5000_single_pass/roll_index.py|c99c54a434d0d53e94e51ed503f0289709b5f20365e16606f365264a617c8b17|38013a1e942c3d1d1798ca0e718fb7ccdc3bc605277ca729e9891fa53bcde311" # its export surface for the class above "protocol/ls5000_single_pass/__init__.py|ce8aa97b707f5ef83f96128b378722191f7280bd41c1f3acbb04c75e3ea7523e|1f0f324034a95e2c8ca772ce52a78a800b0bf215d3ae4ec77422b08b1376856c" - # pins differ because the two files above differ; re-pinned for 0.7.10 - "protocol/ls5000_single_pass/bundle.py|5f0bef275dacad093fe91a93859e9b62c5fc2106f04f25be66e39f0a3908a188|21546dd8e6814e499668c4e973d3a960cd0bee46943af75393a4c52bf1aa3e56" + # pins differ because the two files above differ; re-pinned for 0.7.11 + "protocol/ls5000_single_pass/bundle.py|d75c7393d14b7fe824ccdda201edeac7c144dbe30ff5dd4ac8ca3d9bc8864e71|8cd00cfaaf96d2034e9bae03226a484732ef4a2dfc55f6543fe5ddbbd5ef8d1d" # packaged-app libusb resolution (app bundles its own signed binary) "protocol/ls5000_single_pass/usb_backend.py|afb5b3cbb57404b758f4f8d8795f4307c07c8f6d01bbeccb3ced38026787fd62|666a476ce706a4a854aac50116575e7143f5a1a7c1b1085125347696d89348d1" - # capture-timing receipt fields (started_at/duration); re-pinned for 0.7.10 - "_roll.py|be7d1195dbad38cdfb029fb6959e47989c457832ab11e5ce276b88986a147639|291ef561220e60d95f83cea23b0db62fb95ff18b8c10099644cbadeea985212b" - "capture/single_pass_workflow.py|f1e6921197f10bf3210a7ad673074e0bd1327ab7332f4330019b1426f3f35748|2522b3ef4f12c04de2dcd41fe73dc650b754375d91be8654b162bde599f3c6a7" + # capture-timing receipt fields (started_at/duration); re-pinned for 0.7.11 + "_roll.py|884c8e3702081d72234e98a483413eb334bb34c8e906fc271548594f95a3ffc4|d9a83c6e2d01815844cbecb302a3bb0092de8c71e8394d69c1801e6011ef39fd" + "capture/single_pass_workflow.py|acf15f49722e6cdaba733b7f69f1193532b83fd85880d6f31efcdb6a109ed4ef|fe62a5f40fb82e5f4b6a1a2584f33390d0cd90b9c528f7feb9d91d1d0a92765f" "types.py|487603c8de4a43d8f28ac6ec358c442b66e6c6a80e554308b301f46d33ce6c7d|812efed1f289e6b086269fa50a2fdf49228570eab8f3f06d0d66db2d3489747d" ) workdir="$(mktemp -d)" trap 'rm -rf "$workdir"' EXIT -echo "fetching authenticated coolscanpy 0.7.10 source from PyPI..." +echo "fetching authenticated coolscanpy 0.7.11 source from PyPI..." published_root="$(python3 -I -S -B scripts/fetch_pinned_coolscanpy_sdist.py \ --destination "$workdir/published")" published_src="$published_root/src/coolscanpy" test -d "$published_src" -pypi_version="0.7.10" +pypi_version="0.7.11" printf '%s\n' "${KNOWN_VENDORED_DIVERGENCE[@]}" > "$workdir/exemptions" if PUBLISHED_SRC="$published_src" VENDORED_DIR="$VENDORED_DIR" \ diff --git a/scripts/fetch_pinned_coolscanpy_sdist.py b/scripts/fetch_pinned_coolscanpy_sdist.py index 5e7e1da..845e9f4 100644 --- a/scripts/fetch_pinned_coolscanpy_sdist.py +++ b/scripts/fetch_pinned_coolscanpy_sdist.py @@ -16,16 +16,16 @@ import urllib.request -VERSION = "0.7.10" +VERSION = "0.7.11" FILENAME = f"coolscanpy-{VERSION}.tar.gz" -URL = "https://files.pythonhosted.org/packages/f3/60/d40bccc26d5996dfc9d96b2e052cc9d98d68f4970b0f0b45e7c9b39f9551/coolscanpy-0.7.10.tar.gz" -SIZE = 577225 -SHA256 = "6343a4446e39920c5c381329d220b67d8b1774d1e999566eaf6c5ff4d89ed2e5" +URL = "https://files.pythonhosted.org/packages/b4/2a/90efa7ca5f46a645df8c2097039dc9316b825a9e3b793d983e6a13d8c032/coolscanpy-0.7.11.tar.gz" +SIZE = 577801 +SHA256 = "7fe33118a80ca546e23fb23c1d1567d6e7d31d1c3933c0c94ce9b2b86bc3a97d" ROOT = f"coolscanpy-{VERSION}" ENTRY_COUNT = 104 FILE_COUNT = 89 DIRECTORY_COUNT = 15 -EXPANDED_FILE_BYTES = 2574826 +EXPANDED_FILE_BYTES = 2577037 class FetchError(RuntimeError):