Files
Copies/copienator/commands/enonce_info.py
T

302 lines
11 KiB
Python

from __future__ import annotations
import argparse
import glob
import json
import os
import re
import urllib.request
from collections.abc import Sequence
from uuid import uuid4
from copienator import (
CliError,
EvaluationWorkspace,
ExitCode,
atomic_write_text,
evaluation_parser,
execute,
workspace_from_args,
)
from copienator.platform import WindowsLabelError, validate_windows_labels
from copienator.utils import compile_to_pdf
def fetch_and_save_sub_text(ex_id, indices, label, text_path):
"""Fetches text for a specific sub-question and saves it to Text/{label}.tex"""
qinds = ",".join(map(str, indices))
if qinds:
url = f"http://localhost:8080/exercices/exo_q_text/{ex_id}/{qinds}"
else:
url = f"http://localhost:8080/exercices/exo_q_text/{ex_id}"
try:
with urllib.request.urlopen(url) as response:
content = response.read().decode('utf-8')
content = replace_dots(content.strip("\n"))
with open(os.path.join(text_path, f"{label}.tex"), 'w', encoding='utf-8') as f:
f.write(content)
# Compile PDF
pdf_file = os.path.join(text_path, f"{label}.pdf")
compile_to_pdf(content, pdf_file)
except Exception as e:
print(f"Error fetching sub-text from {url}: {e}")
raise
def fetch_and_save_sub_sol(ex_id, indices, label, sol_path):
"""Fetches text for a specific sub-question and saves it to Text/{label}.tex"""
qinds = ",".join(map(str, indices))
if qinds:
url = f"http://localhost:8080/exercices/exo_q_sol/{ex_id}/{qinds}"
else:
url = f"http://localhost:8080/exercices/exo_q_sol/{ex_id}"
try:
with urllib.request.urlopen(url) as response:
content = response.read().decode('utf-8')
content = replace_dots(content.strip("\n"))
with open(os.path.join(sol_path, f"{label}.tex"), 'w', encoding='utf-8') as f:
f.write(content)
# Compile PDF
pdf_file = os.path.join(sol_path, f"{label}.pdf")
compile_to_pdf(content, pdf_file)
except Exception as e:
print(f"Error fetching sub-text from {url}: {e}")
raise
ROMANS_CAP = ["", "I", "II", "III", "IV", "V", "VI", "VII", "VIII", "IX", "X"]
ROMANS_LOW = ["", "i", "ii", "iii", "iv", "v", "vi", "vii", "viii", "ix", "x"]
def replace_dots(text):
# (?m) enables multiline mode so ^ matches start of each line
return re.sub(r"(?m)^(\s*.)\.", r"\1)", text)
def replace_problem_labels(text):
"""Replaces labels according to spaces depth when problem=True."""
def repl(m):
spaces = m.group(1)
label = m.group(2)
n = len(spaces)
try:
if n == 1 and label.isdigit(): # 1 space: 1) -> I)
return f"{spaces}{ROMANS_CAP[int(label)]})"
elif n == 4 and label.isalpha(): # 4 spaces: a) -> 1)
return f"{spaces}{ord(label.lower()) - 96})"
elif n == 7 and label.isdigit(): # 7 spaces: 1) -> a)
return f"{spaces}{chr(96 + int(label))})"
elif n == 10 and label.isdigit(): # 10 spaces: 1) -> i)
return f"{spaces}{ROMANS_LOW[int(label)]})"
except (IndexError, ValueError):
pass
return m.group(0)
# Matches start of line, spaces, alphanumeric label, and closing parenthesis
return re.sub(r"(?m)^([ \t]+)([a-zA-Z0-9]+)\)", repl, text)
def format_indices(indices, problem=False):
if not indices: return ""
if not problem:
res = f"{indices[0]})"
if len(indices) > 1: res += f"{chr(96 + indices[1])})"
if len(indices) > 2: res += f"{ROMANS_LOW[indices[2]]})"
return res
else:
res = ""
if len(indices) > 0: res += f"{ROMANS_CAP[indices[0]]})"
if len(indices) > 1: res += f"{indices[1]})"
if len(indices) > 2: res += f"{chr(96 + indices[2])})"
if len(indices) > 3: res += f"{ROMANS_LOW[indices[3]]})"
return res
def save_split_content(text, path, base_fname, problem):
# Always save the main aggregated file
with open(os.path.join(path, base_fname), 'w', encoding='utf-8') as f:
f.write(text)
pattern = re.compile(r"(?m)^([ \t]+)([a-zA-Z0-9]+)\)")
all_matches = list(pattern.finditer(text))
target_spaces = 4 if problem else 1
splits = [m for m in all_matches if len(m.group(1)) == target_spaces]
for i, match in enumerate(splits):
start_idx = match.start()
end_idx = splits[i+1].start() if i + 1 < len(splits) else len(text)
chunk = text[start_idx:end_idx].strip("\n")
label = match.group(2) + ")"
if problem:
# Find the most recent 1-space match before this 4-space match
sec_match = next((m for m in reversed(all_matches)
if len(m.group(1)) == 1 and m.start() < match.start()), None)
if sec_match:
label = f"{sec_match.group(2)}){label}"
sub_fname = f"{base_fname} : {label}"
with open(os.path.join(path, sub_fname), 'w', encoding='utf-8') as f:
f.write(chunk)
def process_directory(workspace: EvaluationWorkspace) -> ExitCode:
directory = str(workspace.root)
# Find the first .tex file in the directory
enonce = workspace.root / "enonce.tex"
tex_files = [str(enonce)] if enonce.is_file() else sorted(glob.glob(os.path.join(directory, "*.tex")))
if not tex_files:
print(f"No .tex file found in {directory}. Looking in /Staging/Interro/")
int_name = workspace.root.name
tex_path = os.path.join(os.path.expanduser("~"), "Prépa/Staging/Interro", f"{int_name}.tex")
if os.path.exists(tex_path):
tex_file = tex_path
else:
raise CliError(
f"No .tex input found in {workspace.root}",
ExitCode.INVALID_WORKSPACE,
)
else:
tex_file = tex_files[0]
# Prepare output directories
paths = {
'Text': os.path.join(directory, "Text"),
'Text2': os.path.join(directory, "Text2"),
'Sol': os.path.join(directory, "Sol"),
'Sol2': os.path.join(directory, "Sol2"),
'Persp': os.path.join(directory, "Persp")
}
for p in paths.values():
os.makedirs(p, exist_ok=True)
labels_file = workspace.labels_file
labels_staging = labels_file.with_name(f".{labels_file.name}.{uuid4().hex}.tmp")
current_ex_num = 1
had_errors = False
exercise_groups = []
# Read entirely to allow chunking
with open(tex_file, 'r', encoding='utf-8') as f_in:
content = f_in.read()
# Split by the specific SHEETINFO tag
blocks = content.split("%%SHEETINFO :")
if len(blocks) == 1:
print(f"No SHEETINFO blocks found in {tex_file}")
return ExitCode.PARTIAL
with open(labels_staging, 'w', encoding='utf-8') as f_labels:
# Skip blocks[0] (content before first SHEETINFO)
for block in blocks[1:]:
parts_line = block.split("\n", 1)
json_str = parts_line[0].strip()
block_content = parts_line[1] if len(parts_line) > 1 else ""
# Check if text until next SHEETINFO block contains \Roman
problem = r"\Roman" in block_content
if not json_str: continue
try:
data = json.loads(json_str)
block_labels = []
# Construct 'ids' parameter
ex_id = str(data['id'])
selection = data.get('select')
if selection is not None:
sel_s = [i+1 for i in selection]
ids = f"{ex_id}.{','.join(map(str, sel_s))}"
else:
ids = ex_id
# 2. Handle Labels
indexes = data.get('indexes', [])
if not indexes:
label = f"Ex {current_ex_num}"
validate_windows_labels([label])
block_labels.append(label)
fetch_and_save_sub_text(ids, [], label, paths['Text2'])
fetch_and_save_sub_sol(ids, [], label, paths['Sol2'])
else:
for item in indexes:
suffix = format_indices(item['indices'], problem)
label = f"Ex {current_ex_num}" + (f" : {suffix}" if suffix else "")
validate_windows_labels([label])
block_labels.append(label)
fetch_and_save_sub_text(ids, item['indices'], label, paths['Text2'])
fetch_and_save_sub_sol(ids, item['indices'], label, paths['Sol2'])
# Construct URL (append pb=true if \Roman matched)
url = f"http://localhost:8080/exercices/emacs/{ids}?pretty=true&all=true&persp=true"
# if problem:
# url += "&pb=true"
# Perform GET request
with urllib.request.urlopen(url) as response:
res_content = response.read().decode('utf-8')
# 4. Split and Save content
parts = res_content.split('###')
# Ensure we have at least 3 parts
while len(parts) < 3:
parts.append("")
t_text = replace_dots(parts[0].strip("\n"))
s_text = replace_dots(parts[1].strip("\n"))
p_text = replace_dots(parts[2].strip("\n"))
# Apply hierarchy depth replace if problem context
if problem:
t_text = replace_problem_labels(t_text)
s_text = replace_problem_labels(s_text)
p_text = replace_problem_labels(p_text)
base_filename = f"Ex {current_ex_num}"
if problem:
save_split_content(t_text, paths['Text'], base_filename, False)
else:
with open(os.path.join(paths['Text'], base_filename), 'w', encoding='utf-8') as f:
f.write(t_text)
save_split_content(s_text, paths['Sol'], base_filename, problem)
save_split_content(p_text, paths['Persp'], base_filename, problem)
for label in block_labels:
f_labels.write(f"{label}\n")
exercise_groups.append(block_labels)
current_ex_num += 1
except WindowsLabelError:
labels_staging.unlink(missing_ok=True)
raise
except json.JSONDecodeError:
print(f"Error decoding JSON in block: {json_str}")
had_errors = True
except Exception as e: # noqa: BLE001 - one malformed exercise is partial
print(f"Error processing block {ex_id if 'ex_id' in locals() else 'unknown'}: {e}")
had_errors = True
labels_staging.replace(labels_file)
atomic_write_text(workspace.label_groups_file,
"".join(", ".join(group) + "\n" for group in exercise_groups))
return ExitCode.PARTIAL if had_errors else ExitCode.SUCCESS
def build_parser() -> argparse.ArgumentParser:
return evaluation_parser("Generate statement metadata from SHEETINFO blocks")
def main(argv: Sequence[str] | None = None) -> int:
parser = build_parser()
return execute(parser, argv, lambda args: process_directory(workspace_from_args(args)))
if __name__ == "__main__":
raise SystemExit(main())