- 기계.json 한 파일 → 폴더 기계/(EQ 키마다 파일 + _머리.json · 차례 = 키 차례) · 옮긴 빌더 build_기계폴더_826_2 (655 대 머리 · 줄 · 차례 대조 뒤 옛 파일 지움) - 읽기 도우미 master_machine_files — cm.load · Store_Cache · Store.read 가 옛 「기계.json」 한 표 꼴로 지어 줌(API · 화면 · 색인 그대로) · 쓰기 = 바뀐 대 파일만 - 기계.json 직접 열던 곳 도우미로: M02_San_Basis_Index · M02_Logic_Standard 지문 · build_용도 · master_keys.adopt · _틀 · _갱신절차 - 저장 몸 M01_MasterData_Store_Save — 전 = 기억 · 후 = 고친 파일만 얕은 사본 · 틀은 고친 줄만 · 조합 · 일위대가 문서는 로직 줄이 빠질 때만 · 재료 찾기 기억 이어받음 · 저장 뒤 쓴 파일만 기억에 갈아 끼움 - 마스터 사본 시험 22 개 = 기계 폴더도 복사 · 821_2 · 815_2 글 대조 = 대 파일 하나 대조(옛 한 파일 글 대조 낡음) - 새 시험 test_826_2 — 폴더 = 한 표 · 한 대 한 파일 · 새 · 지움 · 409 · 옛 길과 같은 오류 스무 견본 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
143 lines
5.6 KiB
Python
143 lines
5.6 KiB
Python
"""M01 기계 = 폴더 `기계/` — 한 대가 파일 하나(826-2).
|
|
|
|
`기계/<키>.json` = 줄 하나 · `기계/_머리.json` = 머리(그룹 · 원문 · 구분) · 줄 차례 = 키 차례
|
|
(새 기계는 늘 다음 키라 끝에 붙음 · 옛 한 파일 차례와 같음).
|
|
읽는 쪽은 옛 한 파일 꼴 그대로 — `table()` 이 「기계.json」 {그룹, 원문, 구분, 줄} 로 지어 줌
|
|
(색인 · 계산식 · 화면 · API 는 그 이름을 그대로 씀). 쓰는 쪽은 바뀐 대만 — `write()`.
|
|
폴더가 없고 옛 `기계.json` 파일이 있으면 그 파일을 읽음(시험이 만든 작은 마스터).
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
import os
|
|
from decimal import Decimal
|
|
from pathlib import Path
|
|
|
|
NAME = "기계.json" # 표 꼴 이름(메모리 · API)
|
|
DIR = "기계"
|
|
HEAD = "_머리.json"
|
|
_MEMO: dict[str, tuple] = {} # 파일 경로 → (자국, 바이트, 읽은 줄)
|
|
|
|
|
|
def here(folder: Path) -> bool:
|
|
return (Path(folder) / DIR / HEAD).is_file()
|
|
|
|
|
|
def names(folder: Path) -> list[str]:
|
|
"""첫 층 표 이름 — `*.json` 파일 + 기계 폴더(「기계.json」) · 이름 차례."""
|
|
out = {p.name for p in Path(folder).glob("*.json")}
|
|
return sorted(out | {NAME}) if here(folder) else sorted(out)
|
|
|
|
|
|
def _entries(folder: Path) -> list[os.DirEntry]:
|
|
with os.scandir(Path(folder) / DIR) as it:
|
|
return sorted(
|
|
(e for e in it if e.name.endswith(".json") and not e.name.startswith(".")),
|
|
key=lambda e: e.name,
|
|
)
|
|
|
|
|
|
def stamp(folder: Path) -> tuple:
|
|
"""폴더 자국 — 파일마다 (이름, 시각, 크기)(Windows 는 목록이 자국을 같이 줌)."""
|
|
return tuple((e.name, e.stat().st_mtime_ns, e.stat().st_size) for e in _entries(folder))
|
|
|
|
|
|
def _parse(raw: bytes):
|
|
return json.loads(raw, parse_float=Decimal, parse_int=Decimal)
|
|
|
|
|
|
def _read(entry: os.DirEntry) -> tuple[bytes, object]:
|
|
"""(바이트, 읽은 것) — 자국이 같으면 기억한 것(읽은 것은 **읽기 전용**으로만 나눔)."""
|
|
st = entry.stat()
|
|
sig = (st.st_mtime_ns, st.st_size)
|
|
got = _MEMO.get(entry.path)
|
|
if got is None or got[0] != sig:
|
|
raw = Path(entry.path).read_bytes()
|
|
got = _MEMO[entry.path] = (sig, raw, _parse(raw))
|
|
return got[1], got[2]
|
|
|
|
|
|
def _parts(folder: Path, shared: bool, parse=_parse) -> tuple[list[bytes], dict, list]:
|
|
raws, head, rows = [], {}, []
|
|
for entry in _entries(folder):
|
|
raw, data = _read(entry)
|
|
data = data if shared else parse(raw)
|
|
raws.append(entry.name.encode() + b"\0" + raw)
|
|
if entry.name == HEAD:
|
|
head = data
|
|
elif not entry.name.startswith("_"):
|
|
rows.append(data)
|
|
return raws, head, rows
|
|
|
|
|
|
def table(folder: Path, shared: bool = False, exact: bool = True) -> tuple[dict, str]:
|
|
"""(옛 한 파일 꼴, 판) — 판 = 머리 · 줄 파일 내용 지문. `shared` = 기억한 줄을 그대로 나눔
|
|
(읽기 전용 기억 `Store_Cache` 만) · 아니면 새로 읽은 사본(고쳐도 됨) · `exact` 아니면 수 = int · float."""
|
|
parse = _parse if exact else json.loads
|
|
if not here(folder):
|
|
raw = (Path(folder) / NAME).read_bytes()
|
|
return parse(raw), hashlib.sha256(raw).hexdigest()[:16]
|
|
raws, head, rows = _parts(folder, shared and exact, parse)
|
|
return {**head, "줄": rows}, hashlib.sha256(b"\n".join(raws)).hexdigest()[:16]
|
|
|
|
|
|
def raw(folder: Path, name: str) -> bytes:
|
|
"""첫 층 표의 내용 바이트(지문용) — 기계 폴더는 파일들을 이은 것."""
|
|
if name == NAME and here(folder):
|
|
return b"\n".join(_parts(folder, True)[0])
|
|
return (Path(folder) / name).read_bytes()
|
|
|
|
|
|
def read(folder: Path, name: str):
|
|
"""첫 층 표 하나(새로 읽은 사본 · 수 = Decimal)."""
|
|
if name == NAME and here(folder):
|
|
return table(folder)[0]
|
|
return _parse((Path(folder) / name).read_bytes())
|
|
|
|
|
|
def text(row) -> str:
|
|
"""줄 파일 글 — 들여쓰기 2칸 · 수는 읽은 글 그대로(M01 저장 글 모양 `Store_Shape.dump`)."""
|
|
import sys
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))
|
|
from M01_MasterData.M01_MasterData_Store_Shape import dump
|
|
|
|
return dump(row, 0, width=0, expand=True)
|
|
|
|
|
|
def _put(path: Path, body: str) -> bool:
|
|
data = body.encode("utf-8")
|
|
if path.is_file() and path.read_bytes() == data:
|
|
return False
|
|
tmp = path.with_name(f".{path.name}.tmp")
|
|
tmp.write_bytes(data)
|
|
os.replace(tmp, path)
|
|
return True
|
|
|
|
|
|
def write(folder: Path, data: dict, old: dict | None = None) -> list[str]:
|
|
"""표 꼴 → 폴더 — 바뀐 대만 씀 · 빠진 대는 지움 · 머리가 바뀌면 머리도. `old` = 고치기 전 표
|
|
(없으면 폴더 전부와 견줌). 돌려줌 = 쓰거나 지운 파일 이름."""
|
|
base = Path(folder) / DIR
|
|
base.mkdir(exist_ok=True)
|
|
rows = {f"{r['키']}.json": r for r in data.get("줄") or []}
|
|
if len(rows) != len(data.get("줄") or []):
|
|
raise ValueError("기계 키 겹침 — 한 대 한 파일")
|
|
head = {k: v for k, v in data.items() if k != "줄"}
|
|
was = None if old is None else {f"{r['키']}.json": r for r in old.get("줄") or []}
|
|
done = []
|
|
if (old is None or {k: v for k, v in old.items() if k != "줄"} != head) and _put(
|
|
base / HEAD, text(head)
|
|
):
|
|
done.append(HEAD)
|
|
for name, row in rows.items():
|
|
if (was is None or was.get(name) != row) and _put(base / name, text(row)):
|
|
done.append(name)
|
|
gone = set(was) - set(rows) if was is not None else {e.name for e in _entries(folder)}
|
|
for name in sorted(gone - set(rows) - {HEAD}):
|
|
(base / name).unlink(missing_ok=True)
|
|
done.append(name)
|
|
return done
|