brain backup 2026-09-14
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a local-only Grade 3 catalog from verified Core Knowledge archives."""
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import re
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
|
||||
ROLE_ORDER = {'Student Reader': 0, 'Teacher Guide': 1, 'Reference': 2}
|
||||
|
||||
|
||||
def is_grade_three_source(item):
|
||||
title = str(item.get('title', ''))
|
||||
filename = str(item.get('filename', ''))
|
||||
return bool(re.search(r'\bGrade 3\b', title, re.I) or re.search(r'(?:^|[_-])G3(?:[_-]|$)', filename, re.I))
|
||||
|
||||
|
||||
def classify_unit(filename):
|
||||
if re.search(r'CKMath', filename, re.I):
|
||||
return 'Math'
|
||||
if re.search(r'CKSci', filename, re.I):
|
||||
return 'Science'
|
||||
if re.search(r'CKLA', filename, re.I):
|
||||
return 'Language Arts'
|
||||
if re.search(r'CKHG|CKIYS|LABB_CKHG', filename, re.I):
|
||||
return 'History & Geography'
|
||||
if re.search(r'CKMusic|CKVA', filename, re.I):
|
||||
return 'Arts'
|
||||
return 'Other'
|
||||
|
||||
|
||||
def role_for_pdf(filename):
|
||||
if re.search(r'(?:^|[_-])(TG|TeacherGuide|Teacher[_ -]?Guide)(?:[_-]|\.)', filename, re.I):
|
||||
return 'Teacher Guide'
|
||||
if re.search(r'(?:^|[_-])(SR|StudentReader|Student[_ -]?Reader|ActivityBook)(?:[_-]|\.)', filename, re.I):
|
||||
return 'Student Reader'
|
||||
return 'Reference'
|
||||
|
||||
|
||||
def material_id(archive_name, internal_path):
|
||||
raw = '%s\0%s' % (archive_name, internal_path)
|
||||
return hashlib.sha256(raw.encode('utf-8')).hexdigest()[:20]
|
||||
|
||||
|
||||
def pdf_page_count(archive_path, internal_path):
|
||||
try:
|
||||
import fitz
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
document = fitz.open(stream=archive.read(internal_path), filetype='pdf')
|
||||
count = document.page_count
|
||||
document.close()
|
||||
return count
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def inspect_archive(archive_path, include_page_counts=False):
|
||||
materials = []
|
||||
with zipfile.ZipFile(archive_path) as archive:
|
||||
for info in archive.infolist():
|
||||
internal_path = info.filename
|
||||
if internal_path.startswith('__MACOSX/') or not internal_path.lower().endswith('.pdf'):
|
||||
continue
|
||||
filename = Path(internal_path).name
|
||||
material = {
|
||||
'id': material_id(archive_path.name, internal_path),
|
||||
'filename': filename,
|
||||
'internal_path': internal_path,
|
||||
'role': role_for_pdf(filename),
|
||||
}
|
||||
if include_page_counts:
|
||||
material['pages'] = pdf_page_count(archive_path, internal_path)
|
||||
materials.append(material)
|
||||
materials.sort(key=lambda item: (ROLE_ORDER[item['role']], item['filename'].lower()))
|
||||
return materials
|
||||
|
||||
|
||||
def build_catalog(manifest_path, downloads_dir, include_page_counts=False):
|
||||
entries = json.loads(Path(manifest_path).read_text())
|
||||
units = []
|
||||
for item in entries:
|
||||
if item.get('type') != 'zip' or not is_grade_three_source(item):
|
||||
continue
|
||||
archive_path = Path(downloads_dir) / item['filename']
|
||||
if not archive_path.is_file():
|
||||
continue
|
||||
materials = inspect_archive(archive_path, include_page_counts)
|
||||
units.append({
|
||||
'id': hashlib.sha256(item['filename'].encode('utf-8')).hexdigest()[:16],
|
||||
'title': item['title'],
|
||||
'filename': item['filename'],
|
||||
'source_url': item['source'],
|
||||
'subject': classify_unit(item['filename']),
|
||||
'materials': materials,
|
||||
'teacher_guide_count': sum(material['role'] == 'Teacher Guide' for material in materials),
|
||||
'student_material_count': sum(material['role'] == 'Student Reader' for material in materials),
|
||||
})
|
||||
units.sort(key=lambda item: (item['subject'], item['title'].lower()))
|
||||
return {
|
||||
'grade': 3,
|
||||
'unit_count': len(units),
|
||||
'teacher_guide_count': sum(unit['teacher_guide_count'] for unit in units),
|
||||
'student_material_count': sum(unit['student_material_count'] for unit in units),
|
||||
'units': units,
|
||||
}
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('--manifest', type=Path, required=True)
|
||||
parser.add_argument('--downloads-dir', type=Path, required=True)
|
||||
parser.add_argument('--output', type=Path, required=True)
|
||||
parser.add_argument('--page-counts', action='store_true')
|
||||
args = parser.parse_args()
|
||||
catalog = build_catalog(args.manifest, args.downloads_dir, args.page_counts)
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(catalog, indent=2) + '\n')
|
||||
print(json.dumps({
|
||||
'output': str(args.output),
|
||||
'units': catalog['unit_count'],
|
||||
'teacher_guides': catalog['teacher_guide_count'],
|
||||
'student_materials': catalog['student_material_count'],
|
||||
}))
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Reference in New Issue
Block a user