#!/usr/bin/env python3 """Check the article's snapshot publication example in a disposable directory.""" import argparse import hashlib import platform from datetime import datetime, timezone def validate_snapshot(data): if not isinstance(data, dict): raise ValueError("snapshot must be an object") if type(data.get("version")) is not int: raise ValueError("version must be an integer") if not isinstance(data.get("items"), list): raise ValueError("items must be a list") for item in data["items"]: if not isinstance(item, dict) or type(item.get("id")) is not int: raise ValueError("invalid item") import json import os import tempfile from pathlib import Path def publish_snapshot(target, data): target = Path(target) validate_snapshot(data) candidate = None try: with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", dir=target.parent, prefix=".snapshot-", delete=False ) as stream: candidate = Path(stream.name) json.dump(data, stream, ensure_ascii=False) validate_snapshot(json.loads(candidate.read_text(encoding="utf-8"))) os.replace(candidate, target) finally: if candidate is not None and candidate.exists(): candidate.unlink() def main(): parser = argparse.ArgumentParser() parser.add_argument('--output', required=True) output = Path(parser.parse_args().output) if output.exists(): raise SystemExit('Existing evidence will not be overwritten.') with tempfile.TemporaryDirectory(prefix='punchblog-publish-') as directory: target = Path(directory) / 'snapshot.json' target.write_text(json.dumps({'version': 1, 'items': [{'id': 1}]})) publish_snapshot(target, {'version': 2, 'items': [{'id': n} for n in range(1, 6)]}) expected = target.read_bytes() rows = [['정상 후보 교체', '성공', 2, 5, 0]] errors = [] for label, invalid in [ ('구조 검사 실패', {'version': 3, 'items': 'wrong'}), ('직렬화 실패', {'version': 3, 'items': [], 'extra': object()}) ]: try: publish_snapshot(target, invalid) except (ValueError, TypeError) as error: errors.append(type(error).__name__) else: raise AssertionError('Invalid candidate was accepted') assert target.read_bytes() == expected leftovers = len(list(Path(directory).glob('.snapshot-*'))) assert leftovers == 0 rows.append([label, errors[-1], 2, 5, leftovers]) assert json.loads(expected)['version'] == 2 mode = oct(target.stat().st_mode & 0o777) digest = hashlib.sha256(expected).hexdigest() record = { 'schema_version': 1, 'observed_at': datetime.now(timezone.utc).isoformat(), 'environment': {'os': platform.system(), 'python': platform.python_version()}, 'method': '본문의 validate_snapshot·publish_snapshot 함수로 임시 경로에서 정상 교체, 구조 오류, 직렬화 오류를 순서대로 확인했다. 오류 뒤 정상 파일 바이트와 남은 후보 수를 대조했다.', 'limits': '단일 사용자·단일 writer. fsync·전원 장애·권한 전환·NFS·동시 writer·디스크 부족은 검증하지 않았다.', 'observations': {'last_good_sha256': digest, 'published_mode': mode, 'errors': errors}, 'tables': {'publish': {'headers': ['입력', '결과', '유지된 버전', '항목 수', '남은 후보 파일'], 'rows': rows}} } output.parent.mkdir(parents=True, exist_ok=True) with output.open('x') as stream: json.dump(record, stream, ensure_ascii=False, indent=2) stream.write('\n') print(json.dumps(record['tables'], ensure_ascii=False)) if __name__ == '__main__': main()