cc/tools/publish_materials.py
원본 내용을 읽기 전용으로 표시합니다.
"""E:/proj의 공개용 코드·자료만 별도 묶음으로 복사. 원본 실행/수정은 하지 않는다."""import argparseimport csvimport hashlibimport jsonimport refrom collections import Counterfrom datetime import datetime, timezonefrom pathlib import PathDENIED = {'.git', '.venv', '__pycache__', 'cache', '.pytest_cache', 'artifacts', 'vendor'}SOURCE_EXTENSIONS = {'.py', '.html', '.css', '.js'}SECRET_PATTERNS = [re.compile(r'-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----'),re.compile(r'\b(?:sk-|gw_)[A-Za-z0-9_\-]{25,}'),re.compile(r'''(?i)(?:api[_-]?key|password|passwd|secret[_-]?key|access[_-]?token)\s*[=:]\s*["']([^"'\n]{8,})["']'''),]def candidates(root):chosen = [(p, 'code') for p in root.glob('*.py')]chosen += [(p, 'map' if p.suffix == '.html' else 'image' if p.suffix == '.png' else 'csv' if p.suffix == '.csv' else 'text')for p in (root / 'output').iterdir() if p.is_file() and p.suffix in {'.html', '.png', '.csv', '.txt'}]chosen += [(p, 'csv' if p.suffix == '.csv' else 'text') for p in (root / 'data').iterdir() if p.is_file() and p.suffix in {'.csv', '.geojson'}]for folder in ('startup_app', 'tests', 'tools'):chosen += [(p, 'code') for p in (root / 'cc' / folder).rglob('*') if p.is_file() and p.suffix in SOURCE_EXTENSIONS and not DENIED.intersection(p.relative_to(root).parts)]chosen += [(root / 'cc' / name, 'text') for name in ('requirements.txt', 'requirements-dev.txt')]def order(item):parts = item[0].relative_to(root).partsgroup = 0 if len(parts) == 1 else {'output': 1, 'data': 2, 'cc': 3}.get(parts[0], 4)return group, item[0].as_posix()return sorted(chosen, key=order)def publish(root, output):root, output = root.resolve(), output.resolve()selected = candidates(root)entries, contents = [], []for source, kind in selected:if source.is_symlink() or not source.resolve().is_relative_to(root):raise ValueError(f'허용되지 않은 경로: {source.name}')raw = source.read_bytes()if kind != 'image':text = raw.decode('utf-8-sig')if any(pattern.search(text) for pattern in SECRET_PATTERNS):# 값은 출력하지 않는다. 검토 후 별도로 공개해야 한다.raise ValueError(f'자격증명 가능성이 있어 공개 중단: {source.relative_to(root)}')relative = source.relative_to(root).as_posix()digest = hashlib.sha256(raw).hexdigest()entry = {'id': hashlib.sha256((relative + '\0' + digest).encode()).hexdigest()[:20], 'path': relative, 'kind': kind, 'size': len(raw), 'sha256': digest}if kind == 'csv':with source.open(encoding='utf-8-sig', newline='') as stream:reader = csv.reader(stream)entry['columns'] = len(next(reader, []))entry['rows'] = sum(1 for _ in reader)entries.append(entry)contents.append(raw)for entry, raw in zip(entries, contents):destination = output / 'files' / entry['path']destination.parent.mkdir(parents=True, exist_ok=True)destination.write_bytes(raw)manifest = {'created': datetime.now(timezone.utc).isoformat(), 'files': entries}output.mkdir(parents=True, exist_ok=True)temporary = output / 'manifest.next.json'temporary.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding='utf-8')temporary.replace(output / 'manifest.json')return {'count': len(entries), 'types': dict(Counter(e['kind'] for e in entries)), 'bytes': sum(e['size'] for e in entries)}if __name__ == '__main__':parser = argparse.ArgumentParser()parser.add_argument('--root', type=Path, default=Path('E:/proj'))parser.add_argument('--output', type=Path, default=Path(__file__).resolve().parents[1] / 'public_materials')args = parser.parse_args()print(json.dumps(publish(args.root, args.output), ensure_ascii=False))
파일 확인 정보
SHA-256: 0262b9c5feabe5b97c7e3febbb502a6ca7c3e49274008832dabac66a77088c25