130 lines
4.7 KiB
Python
130 lines
4.7 KiB
Python
#!/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()
|