auto-reporting / scripts-new /generate_wp_report.py
MATIAS Nicolas
patch for discontinued Q
dcfa568
Raw
History Blame Contribute Delete
60.5 kB
#!/usr/bin/env python3
"""Generate LaTeX content files for an ITU-T Working Party report.
Usage:
python generate_wp_report.py <config.json>
Reads a JSON configuration file, fetches data from the ITU website,
and generates LaTeX snippet files in ../wp_doc_template/chapters/variables/.
These snippets are included by the .tex templates in ../wp_doc_template/chapters/.
"""
import sys
import os
# Add script directory to path for imports
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
from common.config import load_wp_config
from common.itu_api import get_documents, get_question, get_study_group, get_working_party, get_work_programme, get_work_item_editors, get_next_sg_meeting
from common.models import Question
from common.utils import (
comma_separated_list, find_td_by_name, find_td_by_number,
find_question_name_td_and_a5, compare_stripped, stripped_starts_with,
is_new_work_item, get_rapporteurs, get_associate_rapporteurs,
get_chairs, get_vice_chairs, get_meeting_reports,
extract_alt_name, extract_new_work_item_info, detect_outgoing_liaisons,
detect_processed_work_items, parse_timing, print_work_programme_summary,
)
from common.latex import (
URL, escape_latex, make_href, write_result, table_row_str, seqsplit_text,
)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
PROJECT_DIR = os.path.dirname(SCRIPT_DIR)
RESULTS_DIR = os.path.join(PROJECT_DIR, 'wp_doc_template', 'chapters', 'variables')
def main():
if len(sys.argv) < 2:
print(f"Usage: {sys.argv[0]} <config.json>")
sys.exit(1)
config = load_wp_config(sys.argv[1])
group = config['group']
wp_number = config['workingParty']
place = config['place']
start = config['start']
start_string = config['startString']
start_date = config['startDate']
end = config['end']
next_meeting = config['next_meeting']
# ---------------------------------------------------------------
# Fetch data from ITU website
# ---------------------------------------------------------------
print("Fetching study group structure...")
sg_details = get_study_group(group=group, start=start_string)
# Find questions under this WP
questions = None
working_party = None
for wp in sg_details.workingParties:
if wp.number == wp_number:
working_party = wp
questions = wp.questions
break
if working_party is None:
print(f"Working party {wp_number} not found in study group {group}")
sys.exit(1)
print("Fetching working party details...")
wp_details = get_working_party(
group=group, working_party=wp_number,
questions=questions, start=start_string
)
# Fetch details for each question
question_numbers = []
questions_details = []
for q in questions:
question_numbers.append(q.number)
print(f" Fetching Q{q.number}/{group}...")
qd = get_question(group=group, question=q.number, start=start_date)
if qd is None:
print(f" Warning: Q{q.number}/{group} not found on ITU page, using empty placeholder")
qd = Question(group=group, question=q.number)
questions_details.append(qd)
# ---------------------------------------------------------------
# Fetch documents
# ---------------------------------------------------------------
print("Fetching documents...")
c_rows = get_documents(document_type="C", group=group, working_party=wp_number,
questions=question_numbers, start=start_date)
plen_rows = get_documents(document_type="PLEN", group=group, working_party=wp_number,
questions=question_numbers, start=start_date)
gen_rows = get_documents(document_type="GEN", group=group, working_party=wp_number,
questions=question_numbers, start=start_date)
wp_rows = get_documents(document_type="WP", group=group, working_party=wp_number,
questions=question_numbers, start=start_date)
# ---------------------------------------------------------------
# Process work programme (scraped from ITU website)
# ---------------------------------------------------------------
print("Fetching work programme...")
work_item_details = []
for qn in question_numbers:
print(f" Fetching work programme for Q{qn}/{group}...")
items = get_work_programme(
group=group, question=qn, working_party=wp_number, start=start_string)
work_item_details.extend(items)
work_items = [wi.workItem for wi in work_item_details]
print_work_programme_summary(work_item_details)
print("Fetching work item editors...")
editors = get_work_item_editors(work_item_details)
approval = []
determination = []
consent = []
non_normative = []
candidate_next = []
rapporteur_meetings = []
new_work_items = []
interim_meetings = []
# Auto-detect approval/consent/determination/agreement from TD documentType
# (parsed from TD title, e.g., "Approval - X.1234: Title")
td_to_work_item = {}
for row in wp_rows:
val = row.number.value.strip()
if row.documentType == 'Approval':
if val not in approval:
approval.append(val)
elif row.documentType == 'Determination':
if val not in determination:
determination.append(val)
elif row.documentType == 'Consent':
if val not in consent:
consent.append(val)
elif row.documentType == 'Agreement':
if val not in non_normative:
non_normative.append(val)
# Build td_to_work_item mapping
work_item_name = row.recommendation or row.acronym
if work_item_name:
for wi in work_item_details:
wi_name = wi.workItem or ""
if wi_name and (work_item_name in wi_name or wi_name in work_item_name):
td_to_work_item[val] = wi
break
# Auto-detect work items progressed (under study items with a TD)
processed_work_items = detect_processed_work_items(work_item_details, wp_rows)
# Auto-detect outgoing liaisons from GEN TDs (LS/O in title)
outgoing_ls = detect_outgoing_liaisons(gen_rows)
# Auto-detect candidate work items for next SG meeting
# Criteria: "Under study" items with timing between end of current meeting and next_meeting
from datetime import timedelta
# Use next_meeting from config if provided, otherwise estimate 6 months out
if next_meeting:
next_meeting_date = next_meeting
else:
next_meeting_date = start + timedelta(days=180)
for wi in work_item_details:
status = (wi.status or '').strip()
if not status.startswith('Under study'):
continue
timing = (wi.timing or '').strip()
if not timing:
continue
timing_date = parse_timing(timing)
# Candidate if timing is between end of current meeting and next meeting
if timing_date and end and timing_date > end and timing_date <= next_meeting_date:
name = wi.workItem or ""
if name and name not in candidate_next:
candidate_next.append(name)
# Auto-detect rapporteur meetings from TDs
for row in wp_rows:
title_lower = (row.title or '').lower()
if (('terms of reference' in title_lower or 'proposal' in title_lower
or 'planned' in title_lower)
and ('rapporteur' in title_lower or 'interim' in title_lower
or 'rgm' in title_lower)):
val = row.number.value.strip()
if val and val not in rapporteur_meetings:
rapporteur_meetings.append(val)
# Scrape next SG meeting info from ITU website
print(f"\nFetching next SG{group} meeting info...")
next_sg_meeting = get_next_sg_meeting(group, after_date=start)
if next_sg_meeting:
print(f" Next SG{group} meeting: {next_sg_meeting['city']}, "
f"{next_sg_meeting['date_range']}")
else:
print(f" Warning: could not find next SG{group} meeting on ITU website")
# Find agenda and report TDs
agenda_title = f"Agenda of WP{wp_number}/{group}"
report_title = f"Report of WP{wp_number}/{group}"
agenda = ""
agenda_td_number = ""
report = ""
report_number = ''
for row in wp_rows:
title_lower = (row.title or "").lower().strip()
if compare_stripped(row.title, agenda_title) or \
(f"agenda" in title_lower and f"wp{wp_number}" in title_lower.replace(' ', '')):
agenda_td_number = f"TD{row.number.value.replace(' ', '')}"
agenda = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
break
for row in wp_rows:
# Match "Report of WP1/17" or "Report of WP1/17 meeting" etc.
if stripped_starts_with(report_title, row.title):
report_number = row.number.value.replace(' ', '')
report = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
break
chairs = get_chairs(wp_details)
vice_chairs = get_vice_chairs(wp_details)
year = int(start_date[2:4])
period = str(int(year / 4) * 4 + 1)
# ---------------------------------------------------------------
# Generate LaTeX snippets
# ---------------------------------------------------------------
os.makedirs(RESULTS_DIR, exist_ok=True)
print(f"\nGenerating LaTeX snippets to {RESULTS_DIR}/")
# Sessions and meeting_days are optional - use None as default for manual entry
sessions = None
meeting_days = []
# --- Variables ---
_generate_variables(group, wp_number, question_numbers, place, start,
end, start_string, start_date, period, chairs, vice_chairs,
report_number, working_party, questions_details)
# --- Frontmatter contacts ---
_generate_frontmatter_contacts(wp_details, wp_number)
# --- Report sections ---
_gen_introduction(group, wp_number, place, start, end, chairs, vice_chairs,
agenda, agenda_td_number, sessions, meeting_days)
_gen_executive_summary(group, wp_number, approval, determination, consent,
non_normative, processed_work_items, outgoing_ls)
_gen_structure_leadership(group, wp_number, chairs, vice_chairs,
question_numbers, questions_details)
_gen_documentation(group, wp_number, question_numbers, start_date, period,
c_rows, plen_rows, gen_rows, wp_rows)
_gen_opening_plenary(group, wp_number, question_numbers, wp_rows)
_gen_opening_plenary_issues(group, wp_number, question_numbers,
wp_rows, plen_rows, gen_rows)
_gen_appointments(group, wp_number, question_numbers, questions_details)
_gen_question_meetings(group, wp_number, question_numbers, wp_rows, c_rows,
gen_rows, work_item_details)
_gen_draft_recommendations(group, wp_number, wp_rows, approval, determination,
consent, non_normative,
td_to_work_item=td_to_work_item)
_gen_outgoing_liaisons(group, wp_number, question_numbers, outgoing_ls, gen_rows)
_gen_work_programme(group, wp_number, question_numbers, work_items,
work_item_details, new_work_items,
wp_rows, editors=editors)
_gen_candidate_work_items(group, wp_number, candidate_next, wp_rows,
work_item_details, editors=editors)
_gen_planned_meetings(group, wp_number, question_numbers, questions,
rapporteur_meetings, wp_rows)
_gen_scheduled_meetings(group, wp_number, interim_meetings, next_sg_meeting)
_gen_ipr(group, wp_number, wp_rows)
_gen_conclusion(group, wp_number, chairs, sg_roles=wp_details.sg_roles)
_gen_annex_a_docs(group, wp_number, c_rows, gen_rows, plen_rows, wp_rows)
_gen_annex_b_reflectors(group, question_numbers)
_gen_annex_c_new_work_items(wp_number, wp_rows)
print("\nDone.")
# ===================================================================
# Variables
# ===================================================================
def _format_date_range(start_dt, end_dt):
"""Format date range as 'D - D Month YYYY' or 'D Month - D Month YYYY'.
Examples:
Same month: 3 - 11 December 2025
Cross-month: 28 November - 5 December 2025
"""
if end_dt is None:
return f"{start_dt.day} {start_dt.strftime('%B')} {start_dt.year}"
if start_dt.month == end_dt.month and start_dt.year == end_dt.year:
return (f"{start_dt.day} - {end_dt.day} "
f"{end_dt.strftime('%B')} {end_dt.year}")
return (f"{start_dt.day} {start_dt.strftime('%B')} - "
f"{end_dt.day} {end_dt.strftime('%B')} {end_dt.year}")
def _generate_variables(group, wp_number, question_numbers, place, start,
end, start_string, start_date, period, chairs, vice_chairs,
report_number, working_party, questions_details):
"""Generate results/variables.tex with LaTeX macro definitions."""
year = int(start_date[0:4])
first_year = int(year / 4) * 4 + 1
last_year = first_year + 3
q_list = ", ".join(f"{qn}/{group}" for qn in question_numbers)
leadership = " and ".join(chairs) if chairs else ""
# Date range: "3 - 11 December 2025"
occurence = _format_date_range(start, end)
# Abstract: include WP title and question titles
wp_title = working_party.title if working_party and working_party.title else ""
q_parts = []
for idx, qn in enumerate(question_numbers):
qd = questions_details[idx] if idx < len(questions_details) else None
q_title = qd.title if qd and qd.title else ""
q_parts.append(f"Q{qn}/{group} ({q_title})" if q_title else f"Q{qn}/{group}")
if len(q_parts) > 1:
q_enum = ", ".join(q_parts[:-1]) + ", and " + q_parts[-1]
elif q_parts:
q_enum = q_parts[0]
else:
q_enum = ""
wp_desc = f"WP{wp_number}/{group}"
if wp_title:
wp_desc = f"WP{wp_number}/{group} ({escape_latex(wp_title)})"
abstract = f"This is a report of {wp_desc}. The {wp_desc} consists of {q_enum}."
lines = [
f"\\newcommand{{\\studyGroup}}{{{group}}}",
f"\\newcommand{{\\workingParty}}{{{wp_number}}}",
f"\\newcommand{{\\questions}}{{{q_list}}}",
f"\\newcommand{{\\place}}{{{place}}}",
f"\\newcommand{{\\occurence}}{{{occurence}}}",
f"\\newcommand{{\\startDate}}{{{start_string}}}",
f"\\newcommand{{\\studyPeriod}}{{{first_year}-{last_year}}}",
f"\\newcommand{{\\period}}{{{period}}}",
f"\\newcommand{{\\leadership}}{{{leadership}}}",
f"\\newcommand{{\\reportNumber}}{{{report_number}}}"
f"\\newcommand{{\\abstr}}{{{abstract}}}",
]
write_result(RESULTS_DIR, "00-variables.tex", "\n".join(lines) + "\n")
def _generate_frontmatter_contacts(wp_details, wp_number):
"""Generate results/frontmatter-contacts.tex with contact longtable rows."""
lines = []
for role in wp_details.roles:
if role.roleName not in ("Chair", "Co-Chair", "Vice-chair"):
continue
name = f"{role.firstName} {role.lastName}".strip()
country = escape_latex(role.address)
title_line = f"WP{wp_number} {role.roleName}"
contact_info = []
if role.tel:
contact_info.append(f"Tel: {escape_latex(role.tel)}")
if role.email:
contact_info.append(f"Email: \\ul{{{escape_latex(role.email)}}}")
lines.append("\\begin{longtable}{p{0.15\\linewidth} p{0.30\\linewidth} p{0.50\\linewidth}}")
lines.append("\\midrule")
newline_sep = ' \\newline '
contact_str = newline_sep.join(contact_info)
lines.append(
f"\\textbf{{Contact:}} &\n"
f"{escape_latex(name)}\\newline\n"
f"{country}\\newline\n"
f"{escape_latex(title_line)} &\n"
f"{contact_str} \\\\")
lines.append("\\midrule")
lines.append("\\end{longtable}")
lines.append("")
write_result(RESULTS_DIR, "00-frontmatter-contacts.tex", "\n".join(lines))
# ===================================================================
# Section generators
# ===================================================================
def _gen_introduction(group, wp_number, place, start, end, chairs, vice_chairs,
agenda, agenda_td_number, sessions, meeting_days):
"""01-introduction variables."""
# Chair string
chair_str = escape_latex(" and ".join(chairs)) if chairs else ""
# Vice-chairs / assisted by
if vice_chairs:
assisted_by = ", assisted by " + escape_latex(", ".join(vice_chairs))
else:
assisted_by = ""
# Sessions count
if sessions is not None:
sessions_str = str(sessions)
else:
sessions_str = "\\textit{number}"
# Meeting days
if meeting_days:
def _format_day(dt):
day = dt.day
if 11 <= day <= 13:
suffix = "th"
elif day % 10 == 1:
suffix = "st"
elif day % 10 == 2:
suffix = "nd"
elif day % 10 == 3:
suffix = "rd"
else:
suffix = "th"
return f"{dt.strftime('%B')} {day}{suffix} {dt.year}"
day_strs = [_format_day(d) for d in meeting_days]
if len(day_strs) == 1:
days_str = day_strs[0]
elif len(day_strs) == 2:
days_str = f"{day_strs[0]} and {day_strs[1]}"
else:
days_str = ", ".join(day_strs[:-1]) + f", and {day_strs[-1]}"
else:
days_str = "\\textit{days}"
# Agenda reference
if agenda:
agenda_ref = agenda
elif agenda_td_number:
agenda_ref = agenda_td_number
else:
agenda_ref = "\\textit{agenda TD}"
lines = [
f"\\newcommand{{\\wpChairs}}{{{chair_str}}}",
f"\\newcommand{{\\assistedBy}}{{{assisted_by}}}",
f"\\newcommand{{\\sessionsCount}}{{{sessions_str}}}",
f"\\newcommand{{\\meetingDays}}{{{days_str}}}",
f"\\newcommand{{\\agendaRef}}{{{agenda_ref}}}",
]
write_result(RESULTS_DIR, "01-introduction.tex", "\n".join(lines) + "\n")
def _gen_executive_summary(group, wp_number, approval, determination, consent,
non_normative, processed_work_items, outgoing_ls):
"""02-executive-summary content."""
lines = [
f"During this SG{group} meeting, WP{wp_number}/{group} achieved the following results:\n",
"\\begin{itemize}",
f" \\item Recommendations finalized and proposed for TAP Approval: {len(approval)}",
f" \\item Recommendations finalized and proposed for TAP Consent: {len(consent)}",
f" \\item Recommendations finalized and proposed for TAP Determination: {len(determination)}",
f" \\item Recommendations finalized and proposed for TAP Agreement: {len(non_normative)}",
f" \\item Work items progressed: {len(processed_work_items)}",
f" \\item Outgoing liaison statements agreed: {len(outgoing_ls)}",
"\\end{itemize}",
]
write_result(RESULTS_DIR, "02-executive-summary-content.tex", "\n".join(lines))
def _gen_structure_leadership(group, wp_number, chairs, vice_chairs,
question_numbers, questions_details):
"""03-wp-structure-and-leadership content."""
lines = [
f"WP{wp_number}/{group} Management team\n",
"\\begin{itemize}",
]
if len(chairs) == 1:
lines.append(f" \\item Chair: {escape_latex(chairs[0])}")
else:
for ch in chairs:
lines.append(f" \\item Co-chair: {escape_latex(ch)}")
for vc in vice_chairs:
lines.append(f" \\item Vice-chair: {escape_latex(vc)}")
lines.append("\\end{itemize}\n")
lines.append(f"The following table reproduces the current list of WP{wp_number}/{group} "
f"Questions and related Rapporteurs\n")
# Question/rapporteur table rows
table_lines = []
for idx, qn in enumerate(question_numbers):
qd = questions_details[idx]
rapps = get_rapporteurs(qd)
assoc_rapps = get_associate_rapporteurs(qd)
n_lines = max(1, len(rapps), len(assoc_rapps))
for i in range(n_lines):
q_name = f"Q{qn}/{group}" if i == 0 else ""
q_title = escape_latex(qd.title or '') if i == 0 else ""
rapp = escape_latex(rapps[i]) if i < len(rapps) else ""
assoc = escape_latex(assoc_rapps[i]) if i < len(assoc_rapps) else ""
table_lines.append(table_row_str([q_name, q_title, rapp, assoc]))
lines.append("") # separator
write_result(RESULTS_DIR, "03-structure-leadership.tex", "\n".join(lines))
rows = "".join(table_lines)
write_result(RESULTS_DIR, "03-structure-questions-table.tex",
f"\\newcommand{{\\questionsTable}}{{\n{rows}}}\n")
def _gen_documentation(group, wp_number, question_numbers, start_date, period,
c_rows, plen_rows, gen_rows, wp_rows):
"""04-documentation-and-email-lists content."""
lines = [f"The following documents were discussed by WP{wp_number}/{group} Questions:\n"]
for qn in question_numbers:
q_name = f"Q{qn}/{group}"
lines.append(f"\\textbf{{{q_name}:}}\n")
lines.append("\\begin{itemize}")
# Contributions for this question
c_links = ", ".join(
make_href(URL + r.number.link, f"C{r.number.value}{r.lastRev}")
for r in reversed(c_rows)
if any(rq.value == q_name for rq in r.questions)
)
lines.append(f" \\item C: {c_links}")
# TDs for this question (GEN + PLEN + WP)
td_links = []
for r in reversed(gen_rows):
if any(rq.value == q_name for rq in r.questions):
td_links.append(make_href(URL + r.number.link,
f"TD{r.number.value}{r.lastRev}"))
for r in reversed(plen_rows):
if any(rq.value == q_name for rq in r.questions):
td_links.append(make_href(URL + r.number.link,
f"TD{r.number.value}{r.lastRev}"))
for r in reversed(wp_rows):
if any(rq.value == q_name for rq in r.questions):
td_links.append(make_href(URL + r.number.link,
f"TD{r.number.value}{r.lastRev}"))
lines.append(f" \\item TD: {', '.join(td_links)}")
lines.append("\\end{itemize}\n")
doc_url = f"{URL}/md/T{period}-SG{group}-{start_date[2:]}/sum/en"
lines.append(f"The complete documentation for this SG{group} meeting is to be found at:\n")
lines.append(f"\\href{{{doc_url}}}{{{doc_url}}}\n")
write_result(RESULTS_DIR, "04-documentation-links.tex", "\n".join(lines))
# Email info
year = int(start_date[0:4])
first_year = int(year / 4) * 4 + 1
last_year = first_year + 3
ifa_url = f"{URL}/en/ITU-T/studygroups/{first_year}-{last_year}/{group}/Pages/ifa-structure.aspx"
sub_url = f"{URL}/net4/iwm?p0=0&p11=ITU&p12=ITU-SEP-ITU-T-SEP-SP%2017-SEP-Study%20Group%2017&p21=ITU&p22=ITU"
email_lines = [
f"E-mail correspondences pertaining to the activities of this working party and "
f"Questions under this working party are routinely conducted using the e-mail reflectors. "
f"For more information on available e-mail reflectors and informal FTP areas, please visit "
f"the dedicated \\href{{{URL}{ifa_url}}}{{webpage}}.\n",
f"Those wishing to subscribe or unsubscribe to SG{group} email reflectors, "
f"please visit \\href{{{sub_url}}}{{subscription webpage}}.\n",
]
write_result(RESULTS_DIR, "04-email-info.tex", "\n".join(email_lines))
def _gen_opening_plenary(group, wp_number, question_numbers, wp_rows):
"""05-opening-plenary-results content (interim meeting reports table)."""
lines = []
total_reports = 0
for qn in question_numbers:
reports = get_meeting_reports(wp_rows, qn, group)
total_reports += len(reports)
if total_reports > 0:
table_lines = []
for qn in question_numbers:
q_name = f"Q{qn}/{group}"
meeting_name = f"{q_name} Rapporteur meeting"
reports = get_meeting_reports(wp_rows, qn, group)
for report in reports:
location = ""
date = ""
idx1 = report.title.rfind('(')
if idx1 >= 0:
idx2 = report.title.find(')', idx1)
idx3 = report.title.find(',', idx1)
if idx2 >= 0 and idx3 >= 0:
location = report.title[idx1 + 1:idx3]
date = report.title[idx3 + 1:idx2]
td_link = make_href(URL + report.number.link,
f"TD{report.number.value}/{wp_number}")
table_lines.append(table_row_str([
q_name, meeting_name, f"{location}, {date}", td_link
]))
rows = "".join(table_lines)
write_result(RESULTS_DIR, "05-opening-plenary-reports.tex",
f"\\newcommand{{\\openingPlenaryReports}}{{\n{rows}}}\n")
else:
write_result(RESULTS_DIR, "05-opening-plenary-reports.tex",
"\\newcommand{\\openingPlenaryReports}{}\n")
def _gen_opening_plenary_issues(group, wp_number, question_numbers,
wp_rows, plen_rows, gen_rows):
"""05-opening-plenary — Section 5.2: other issues discussed at opening plenary."""
all_rows = list(wp_rows) + list(plen_rows) + list(gen_rows)
# Categorise TDs by topic
incoming_ls = []
workshops = []
tutorials = []
joint_sessions = []
for row in all_rows:
title_lower = (row.title or "").lower()
if ("ls from" in title_lower or "liaison statement from" in title_lower
or "incoming liaison" in title_lower):
incoming_ls.append(row)
if "workshop" in title_lower:
workshops.append(row)
if "tutorial" in title_lower:
tutorials.append(row)
if "joint session" in title_lower or "joint meeting" in title_lower:
joint_sessions.append(row)
lines = []
if incoming_ls:
lines.append("\\textbf{Review of incoming liaison statements:}\n")
lines.append("\\begin{itemize}")
for row in incoming_ls:
td_link = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
title = escape_latex(row.title or "")
lines.append(f" \\item {td_link}: {title}")
lines.append("\\end{itemize}\n")
if workshops:
lines.append("\\textbf{Workshops:}\n")
lines.append("\\begin{itemize}")
for row in workshops:
td_link = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
title = escape_latex(row.title or "")
lines.append(f" \\item {td_link}: {title}")
lines.append("\\end{itemize}\n")
if tutorials:
lines.append("\\textbf{Tutorials:}\n")
lines.append("\\begin{itemize}")
for row in tutorials:
td_link = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
title = escape_latex(row.title or "")
lines.append(f" \\item {td_link}: {title}")
lines.append("\\end{itemize}\n")
if joint_sessions:
lines.append("\\textbf{Joint sessions:}\n")
lines.append("\\begin{itemize}")
for row in joint_sessions:
td_link = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
title = escape_latex(row.title or "")
lines.append(f" \\item {td_link}: {title}")
lines.append("\\end{itemize}\n")
content = "\n".join(lines) if lines else ""
write_result(RESULTS_DIR, "05-opening-plenary-issues.tex",
f"\\newcommand{{\\openingPlenaryIssues}}{{\n{content}}}\n")
def _gen_appointments(group, wp_number, question_numbers, questions_details):
"""06-appointments — current rapporteurs and officers."""
lines = []
lines.append(f"Current list of Rapporteurs, Associate Rapporteurs and "
f"Liaison Officers for WP{wp_number}/{group}:\n")
lines.append("\\begin{itemize}")
for idx, qn in enumerate(question_numbers):
qd = questions_details[idx] if idx < len(questions_details) else None
if not qd:
continue
q_name = f"Q{qn}/{group}"
rapps = get_rapporteurs(qd)
assoc_rapps = get_associate_rapporteurs(qd)
# Extract liaison officers from question roles
liaison_officers = [
f"{role.firstName} {role.lastName} ({role.company}, {role.address})"
for role in qd.roles
if role.roleName and "liaison" in role.roleName.lower()
]
parts = []
if rapps:
rapp_label = "Rapporteur" if len(rapps) == 1 else "Co-rapporteurs"
parts.append(f"{rapp_label}: {', '.join(rapps)}")
if assoc_rapps:
parts.append(f"Associate Rapporteur{'s' if len(assoc_rapps) > 1 else ''}: "
f"{', '.join(assoc_rapps)}")
if liaison_officers:
parts.append(f"Liaison Officer{'s' if len(liaison_officers) > 1 else ''}: "
f"{', '.join(liaison_officers)}")
if parts:
detail = "; ".join(parts)
lines.append(f" \\item \\textbf{{{q_name}}}: {detail}")
else:
lines.append(f" \\item \\textbf{{{q_name}}}: (no roles found)")
lines.append("\\end{itemize}\n")
content = "\n".join(lines)
write_result(RESULTS_DIR, "06-appointments.tex",
f"\\newcommand{{\\openingPlenaryAppointments}}{{\n{content}}}\n")
def _gen_question_meetings(group, wp_number, question_numbers, wp_rows, c_rows,
gen_rows, work_item_details):
"""07-questions-meetings content — per-question executive summaries.
Generates the same format as Question Report Part 2 for each question.
"""
lines = []
for qn in question_numbers:
q_name = f"Q{qn}/{group}"
lines.append(f"\\subsection{{Question {qn}/{group}}}\n")
# Find question report TD
q_report_title = f"Report of Q{qn}/{group}"
q_report = ""
for row in wp_rows:
if compare_stripped(row.title, q_report_title):
q_report = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}")
break
if q_report:
lines.append(f"The report of Q{qn}/{group} can be found in {q_report}. It was approved.\n")
# Count contributions for this question
q_contributions = [r for r in c_rows
if any(rq.value == q_name for rq in r.questions)]
# Filter WP TDs for this question
q_wp_rows = [r for r in wp_rows if any(rq.value == q_name for rq in r.questions)]
# Filter GEN TDs for this question
q_gen_rows = [r for r in gen_rows if any(rq.value == q_name for rq in r.questions)]
# Detect approval/consent/determination/agreement from documentType (same as Question Report)
q_approval = []
q_determination = []
q_consent = []
q_agreement = []
for row in q_wp_rows:
val = row.number.value.strip()
if row.documentType == 'Approval':
if val not in q_approval:
q_approval.append(val)
elif row.documentType == 'Determination':
if val not in q_determination:
q_determination.append(val)
elif row.documentType == 'Consent':
if val not in q_consent:
q_consent.append(val)
elif row.documentType == 'Agreement':
if val not in q_agreement:
q_agreement.append(val)
# Work items progressed (under study items with a TD)
# wi.question is like "Q10/17", qn is like 10
q_work_item_details = [wi for wi in work_item_details
if wi.question and (f"Q{qn}/{group}" in str(wi.question)
or str(wi.question) == str(qn))]
q_processed = detect_processed_work_items(q_work_item_details, q_wp_rows)
# Outgoing liaison statements from GEN TDs (LS/O in title)
q_outgoing_ls = detect_outgoing_liaisons(q_gen_rows)
# Build executive summary
lines.append(f"During this SG{group} meeting, {q_name} received "
f"{len(q_contributions)} contribution{'s' if len(q_contributions) != 1 else ''} "
f"and achieved the following results:\n")
# Helper to make TD links
def _make_td_links(td_numbers, rows, suffix=""):
links = []
for n in td_numbers:
td = None
for row in rows:
if row.number.value.strip() == str(n).strip():
td = row
break
if td:
links.append(make_href(URL + td.number.link, f"TD {n}{suffix}"))
else:
links.append(f"TD {n}{suffix}")
return links
items = []
# Approval - always show, "None" if empty
if q_approval:
links = _make_td_links(q_approval, q_wp_rows)
items.append(f" \\item Recommendations finalized and proposed for TAP Approval: "
f"{comma_separated_list(links)}")
else:
items.append(f" \\item Recommendations finalized and proposed for TAP Approval: None")
# Consent - always show, "None" if empty
if q_consent:
links = _make_td_links(q_consent, q_wp_rows)
items.append(f" \\item Recommendations finalized and proposed for TAP Consent: "
f"{comma_separated_list(links)}")
else:
items.append(f" \\item Recommendations finalized and proposed for TAP Consent: None")
# Determination - always show, "None" if empty
if q_determination:
links = _make_td_links(q_determination, q_wp_rows)
items.append(f" \\item Recommendations finalized and proposed for TAP Determination: "
f"{comma_separated_list(links)}")
else:
items.append(f" \\item Recommendations finalized and proposed for TAP Determination: None")
# Agreement - always show, "None" if empty
if q_agreement:
links = _make_td_links(q_agreement, q_wp_rows)
items.append(f" \\item Recommendations finalized and proposed for TAP Agreement: "
f"{comma_separated_list(links)}")
else:
items.append(f" \\item Recommendations finalized and proposed for TAP Agreement: None")
# Work items progressed - always show, "None" if empty
if q_processed:
td_links = [make_href(URL + td.number.link, f"TD {td_num}")
for _, td_num, td in q_processed]
items.append(f" \\item Work items progressed: {comma_separated_list(td_links)}")
else:
items.append(f" \\item Work items progressed: None")
# Outgoing liaison statements - always show, "None" if empty
if q_outgoing_ls:
links = _make_td_links(q_outgoing_ls, q_gen_rows, "/G")
items.append(f" \\item Outgoing liaison statements agreed: {comma_separated_list(links)}")
else:
items.append(f" \\item Outgoing liaison statements agreed: None")
lines.append("\\begin{itemize}")
lines.extend(items)
lines.append("\\end{itemize}\n")
write_result(RESULTS_DIR, "07-question-meetings.tex", "\n".join(lines))
def _gen_draft_recommendations(group, wp_number, wp_rows, approval, determination,
consent, non_normative,
td_to_work_item=None):
"""08-draft-recommendations and 09-agreement table rows."""
td_wi = td_to_work_item or {}
def _gen_table(items, has_a5=True):
lines = []
for element in items:
q_name, td, a5 = find_question_name_td_and_a5(wp_rows, element)
final_text = "no update"
if td:
final_text = make_href(URL + td.number.link,
f"TD {td.number.value}{td.lastRev}/{wp_number}")
a5_text = ""
if a5:
a5_text = make_href(URL + a5.number.link,
f"TD {a5.number.value}{a5.lastRev}/{wp_number}")
# Get work item name and version from work programme
wp_wi = td_wi.get(element.strip())
if wp_wi and wp_wi.workItem:
work_item = wp_wi.workItem
elif td and td.acronym:
work_item = f"{td.recommendation}({td.acronym})"
elif td and td.recommendation:
work_item = td.recommendation
elif td:
work_item = td.acronym
else:
work_item = ""
version = escape_latex(wp_wi.version) if wp_wi and wp_wi.version else ""
text_title = escape_latex(td.textTitle) if td else ""
equiv_num = escape_latex(wp_wi.equivNum) if wp_wi and wp_wi.equivNum else ""
# Columns: Question | Work Item | Version | Title | Final Text | A.5 | Equivalent
if has_a5:
lines.append(table_row_str([
q_name, seqsplit_text(work_item), version,
text_title, final_text, a5_text, equiv_num
]))
else:
# Agreement: Question | Work Item | Version | Title | Final Text
lines.append(table_row_str([
q_name, seqsplit_text(work_item), version,
text_title, final_text
]))
return "".join(lines)
# Approval
rows = _gen_table(approval)
has_approval = "true" if approval else "false"
write_result(RESULTS_DIR, "08-draft-rec-approval.tex",
f"\\newcommand{{\\hasWpApproval}}{{{has_approval}}}\n"
f"\\newcommand{{\\wpApproval}}{{\n{rows}}}\n")
# Determination
rows = _gen_table(determination)
has_determination = "true" if determination else "false"
write_result(RESULTS_DIR, "08-draft-rec-determination.tex",
f"\\newcommand{{\\hasWpDetermination}}{{{has_determination}}}\n"
f"\\newcommand{{\\wpDetermination}}{{\n{rows}}}\n")
# Consent
rows = _gen_table(consent)
has_consent = "true" if consent else "false"
write_result(RESULTS_DIR, "08-draft-rec-consent.tex",
f"\\newcommand{{\\hasWpConsent}}{{{has_consent}}}\n"
f"\\newcommand{{\\wpConsent}}{{\n{rows}}}\n")
# Agreement (no A.5 column)
rows = _gen_table(non_normative, has_a5=False)
has_agreement = "true" if non_normative else "false"
write_result(RESULTS_DIR, "09-agreement.tex",
f"\\newcommand{{\\hasWpAgreement}}{{{has_agreement}}}\n"
f"\\newcommand{{\\wpAgreement}}{{\n{rows}}}\n")
def _gen_outgoing_liaisons(group, wp_number, question_numbers, outgoing_ls, gen_rows):
"""10-outgoing-liaison-statements table rows."""
lines = []
for element in outgoing_ls:
q_name, td = find_td_by_number(gen_rows, element)
title = ""
td_name = "no update"
action_to = ""
info_to = ""
if td:
title = td.title
action_to, info_to = _parse_liaison_destinations(title)
td_name = make_href(URL + td.number.link,
f"TD {element}{td.lastRev}/G")
# Get question from TD
if td.questions:
q_name = td.questions[0].value if td.questions else ""
# Combine action_to and info_to in single column (without prefixes)
combined_dest = []
if action_to:
combined_dest.append(escape_latex(action_to))
if info_to:
combined_dest.append(escape_latex(info_to))
destination = " / ".join(combined_dest) if combined_dest else "\\textit{(manual entry)}"
# Columns: Question | Title | For info/action to | TD number
lines.append(table_row_str([
q_name,
escape_latex(title),
destination,
td_name
]))
rows = "".join(lines)
has_outgoing = "true" if outgoing_ls else "false"
write_result(RESULTS_DIR, "10-outgoing-liaisons.tex",
f"\\newcommand{{\\hasWpOutgoingLiaisons}}{{{has_outgoing}}}\n"
f"\\newcommand{{\\wpOutgoingLiaisons}}{{\n{rows}}}\n")
def _gen_work_programme(group, wp_number, question_numbers, work_items,
work_item_details, new_work_items,
wp_rows, editors=None):
"""11-work-programme table rows (new, deleted, ongoing)."""
editors = editors or {}
def _lookup_editor(name):
"""Look up editor by work item name, trying multiple fallback strategies."""
if not name:
return ""
editor = editors.get(name, "")
if editor:
return editor
# Strategy 1: try alt name from "(ex ...)" pattern
alt = extract_alt_name(name)
if alt:
editor = editors.get(alt, "")
if editor:
return editor
# Strategy 2: strip "for new work item " prefix
stripped = name
if stripped.lower().startswith('for new work item '):
stripped = stripped[len('for new work item '):].strip()
editor = editors.get(stripped, "")
if editor:
return editor
# Strategy 3: try TR. <-> XSTR. prefix swap
if stripped.startswith('TR.'):
editor = editors.get('XSTR.' + stripped[3:], "")
if editor:
return editor
elif stripped.startswith('XSTR.'):
editor = editors.get('TR.' + stripped[5:], "")
if editor:
return editor
# Strategy 4: extract base name and search editors dict for partial match
base = stripped.split('(')[0].strip().split(',')[0].strip()
if base and base != stripped:
editor = editors.get(base, "")
if editor:
return editor
for key in editors:
key_alt = extract_alt_name(key)
if key_alt and key_alt.lower() == base.lower():
return editors[key]
return ""
# New work items from WP TDs (look for "new work item" or "NWI" in title)
lines = []
for row in wp_rows:
if is_new_work_item(row.title):
work_item, text_title = extract_new_work_item_info(row.title, wp_rows)
if not work_item:
continue
q_name = row.questions[0].value if row.questions else ""
td_link = make_href(URL + row.number.link,
f"TD {row.number.value.replace(' ', '')}/{wp_number}")
# Columns: Question | Work Item | Status | Title | Editor | Base Text | Equivalent | Approval process
lines.append(table_row_str([
q_name, seqsplit_text(work_item), "New",
escape_latex(text_title), "(manual entry)",
td_link, "", ""
]))
rows = "".join(lines)
has_new = "true" if lines else "false"
write_result(RESULTS_DIR, "11-wp-new-work-items.tex",
f"\\newcommand{{\\hasWpNewWorkItems}}{{{has_new}}}\n"
f"\\newcommand{{\\wpNewWorkItems}}{{\n{rows}}}\n")
# Ongoing work items — only "Under Study" items
lines = []
for wi in work_item_details:
status = wi.status or ""
# Filter: only include "Under Study" items
if not status.lower().startswith('under study'):
continue
name = wi.workItem or ""
title = wi.title or ""
equiv = wi.equivNum or ""
timing = wi.timing or ""
aap = wi.approvalProcess or ""
q_name = wi.question or ""
# Try to find matching WP TD for the base text link
td_name = "no update"
_, td = find_td_by_name(wp_rows, name)
if td is None:
alt = extract_alt_name(name)
if alt:
_, td = find_td_by_name(wp_rows, alt)
if td:
td_name = make_href(URL + td.number.link,
f"TD{td.number.value}{td.lastRev}/{wp_number}")
if not title:
title = td.textTitle
# Columns: Question | Work Item | Status | Title | Editor | Base Text | Equivalent | Target Date | Summary updated | Approval process
lines.append(table_row_str([
q_name, seqsplit_text(name), escape_latex(status),
escape_latex(title), "(manual entry)",
td_name, escape_latex(equiv), escape_latex(timing), "", escape_latex(aap)
]))
rows = "".join(lines)
has_ongoing = "true" if lines else "false"
write_result(RESULTS_DIR, "11-wp-ongoing-work-items.tex",
f"\\newcommand{{\\hasWpOngoingWorkItems}}{{{has_ongoing}}}\n"
f"\\newcommand{{\\wpOngoingWorkItems}}{{\n{rows}}}\n")
def _gen_candidate_work_items(group, wp_number, candidate_next, wp_rows,
work_item_details, editors=None):
"""12-candidate-work-items table rows."""
editors = editors or {}
lines = []
for element in candidate_next:
# Try to find matching WP TD for baseline text
q_name_td, td = find_td_by_name(wp_rows, element)
if td is None:
alt = extract_alt_name(element)
if alt:
q_name_td, td = find_td_by_name(wp_rows, alt)
title = ""
td_name = "no update"
status = ""
equiv = ""
q_name = q_name_td or ""
editor = ""
# Look for baseline text TD
element_lower = element.lower()
alt = extract_alt_name(element)
alt_lower = alt.lower() if alt else ""
for row in wp_rows:
row_title = (row.title or "").lower()
if "baseline text" in row_title:
if element_lower in row_title or (alt_lower and alt_lower in row_title):
td = row
td_name = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}/{wp_number}")
break
# Fill status/title/equiv/question from work programme data
for wi in work_item_details:
wi_name = wi.workItem or ""
wi_alt = extract_alt_name(wi_name)
if (wi_name == element or wi_alt == element
or element in wi_name or (wi_alt and element in wi_alt)):
status = wi.status or ""
if not title:
title = wi.title or ""
equiv = wi.equivNum or ""
if not q_name and wi.question:
q_name = wi.question
break
# Columns: Question | Work Item | Status | Title | Editor | Base Text | A.5 justification | Equivalent
lines.append(table_row_str([
q_name, seqsplit_text(str(element)),
escape_latex(status), escape_latex(title), "(manual entry)",
td_name, "", escape_latex(equiv)
]))
rows = "".join(lines)
has_candidate = "true" if candidate_next else "false"
write_result(RESULTS_DIR, "12-candidate-work-items.tex",
f"\\newcommand{{\\hasWpCandidateWorkItems}}{{{has_candidate}}}\n"
f"\\newcommand{{\\wpCandidateWorkItems}}{{\n{rows}}}\n")
def _gen_planned_meetings(group, wp_number, question_numbers, questions,
rapporteur_meetings, wp_rows):
"""13-planned-interim-meetings table rows."""
lines = []
if rapporteur_meetings:
for element in rapporteur_meetings:
_, td = find_td_by_number(wp_rows, element)
q_name = ""
title = ""
td_ref = ""
date_str = ""
place = ""
contact = ""
if td:
title = escape_latex(td.title)
td_ref = make_href(URL + td.number.link,
f"TD{element}{td.lastRev}/{wp_number}")
# Extract question from TD
if td.questions:
q_name = td.questions[0].value
# Try extract "(Place, Date)" from end of title
raw_title = td.title or ""
idx1 = raw_title.rfind('(')
idx2 = raw_title.rfind(')')
if idx1 >= 0 and idx2 > idx1:
info = raw_title[idx1 + 1:idx2]
parts = info.split(',', 1)
if len(parts) == 2:
place = parts[0].strip()
date_str = parts[1].strip()
# Contact: rapporteur for this question
if q_name:
q_num = q_name.replace(f"/{group}", "")
for qd in questions:
if str(qd.question) == q_num:
for role in qd.roles:
if 'rapporteur' in role.roleName.lower():
contact = f"{role.firstName} {role.lastName}"
break
break
lines.append(table_row_str([
q_name, date_str, place, title or td_ref, contact
]))
else:
for qn in question_numbers:
lines.append(table_row_str([f"{qn}/{group}", "", "", "", ""]))
rows = "".join(lines)
write_result(RESULTS_DIR, "13-planned-meetings.tex",
f"\\newcommand{{\\wpPlannedMeetings}}{{\n{rows}}}\n")
def _gen_scheduled_meetings(group, wp_number, interim_meetings,
next_sg_meeting=None):
"""14-scheduled-meetings content."""
if interim_meetings:
lines = ["\\begin{itemize}"]
for element in interim_meetings:
lines.append(f" \\item {escape_latex(str(element))}")
lines.append("\\end{itemize}")
content = "\n".join(lines)
else:
content = (
f"WP{wp_number}/{group} does not plan to organize a "
f"WP{wp_number}/{group} interim meeting "
f"before the next SG{group} meeting.\n"
)
write_result(RESULTS_DIR, "14-scheduled-meetings.tex",
f"\\newcommand{{\\wpScheduledMeetings}}{{\n{content}}}\n")
# Next SG meeting info for section 13.2
if next_sg_meeting:
city = next_sg_meeting['city']
country = next_sg_meeting.get('country', '')
date_range = next_sg_meeting['date_range']
if country:
location = f"{city}, {country}"
else:
location = city
next_meeting_text = escape_latex(f"{location}, {date_range}")
else:
next_meeting_text = "\\textit{city country, x.ymonth, 20yy}"
write_result(RESULTS_DIR, "14-next-sg-meeting.tex",
f"\\newcommand{{\\wpNextSGMeeting}}{{{next_meeting_text}}}\n")
def _gen_ipr(group, wp_number, wp_rows):
"""04-ipr content: scan TDs for IPR statements, default to none received."""
ipr_statements = []
for row in wp_rows:
title_lower = (row.title or "").lower()
if ("ipr" in title_lower or "intellectual property" in title_lower
or "patent" in title_lower):
td_link = make_href(URL + row.number.link,
f"TD{row.number.value}{row.lastRev}/{wp_number}")
ipr_statements.append(
f" \\item {escape_latex(row.title)} ({td_link})")
if ipr_statements:
content = ("\\begin{itemize}\n"
+ "\n".join(ipr_statements)
+ "\n\\end{itemize}\n")
else:
content = "No IPR statements were received at this meeting.\n"
write_result(RESULTS_DIR, "04-ipr.tex",
f"\\newcommand{{\\wpIPRStatements}}{{\n{content}}}\n")
def _gen_conclusion(group, wp_number, chairs, sg_roles=None):
"""15-conclusion content: dynamic chair and TSB staff names."""
if chairs:
chair_text = " and ".join(chairs)
else:
chair_text = f"WP{wp_number}/{group}"
# Extract TSB staff from SG-level roles
sg_roles = sg_roles or []
counsellor = ""
project_officer = ""
assistant = ""
for role in sg_roles:
name = f"{role.firstName} {role.lastName} ({role.company})"
role_lower = role.roleName.lower()
if 'counsellor' in role_lower and not counsellor:
counsellor = name
elif 'project officer' in role_lower and not project_officer:
project_officer = name
elif 'assistant' in role_lower and not assistant:
assistant = name
counsellor_text = escape_latex(counsellor) if counsellor else "\\textit{counsellor name (TSB)}"
officer_text = escape_latex(project_officer) if project_officer else "\\textit{project officer name (TSB)}"
assistant_text = escape_latex(assistant) if assistant else "\\textit{assistant name (TSB)}"
content = (
f"The {chair_text}, Chair of WP{wp_number}/{group}, "
f"thanked the delegates for their enthusiastic and active participation "
f"in the relevant activities of Working Party {wp_number}/{group} and each "
f"Question during the meeting. Special thanks were expressed to the "
f"SG{group} Counsellor, "
f"{counsellor_text}, "
f"SG{group} Project Officer "
f"{officer_text} "
f"and SG{group} assistant "
f"{assistant_text} "
f"as well as all Associate Rapporteurs, Editors, and contributors "
f"for their dedicated and sustained efforts during this meeting.\n"
)
write_result(RESULTS_DIR, "15-conclusion.tex",
f"\\newcommand{{\\wpConclusion}}{{\n{content}}}\n")
def _gen_annex_a_docs(group, wp_number, c_rows, gen_rows, plen_rows, wp_rows):
"""Annex A: documentation tables (contributions, GEN, PLEN, WP) for all questions."""
def _format_source(name):
"""Format source name: strip study group prefix (escaping done by make_href)."""
if name and name.startswith(f"SG{group}"):
name = name[len(f"SG{group}"):].strip()
if name.startswith("-"):
name = name[1:].strip()
return name or ""
def _get_q_display(row):
"""Get question display string from row, e.g. 'Q10/17' or 'QALL/17'."""
if not row.questions:
return ""
q_val = row.questions[0].value # e.g. "10/17" or "ALL/17"
if q_val.startswith("Q"):
return q_val # Already has Q prefix
return f"Q{q_val}"
# Contributions
lines = []
for row in c_rows:
q_display = _get_q_display(row)
name = make_href(URL + row.number.link, f"C{row.number.value}{row.lastRev}")
source = make_href(URL + row.source.link, _format_source(row.source.name))
lines.append(table_row_str([q_display, name, source, escape_latex(row.title), q_display]))
rows = "".join(lines) if lines else "NONE"
write_result(RESULTS_DIR, "annex-a-contributions.tex",
f"\\newcommand{{\\wpAnnexContributions}}{{\n{rows}}}\n")
# GEN TDs
lines = []
for row in gen_rows:
q_display = _get_q_display(row)
name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/G")
source = make_href(URL + row.source.link, _format_source(row.source.name))
lines.append(table_row_str([q_display, name, source, escape_latex(row.title), q_display]))
rows = "".join(lines) if lines else "NONE"
write_result(RESULTS_DIR, "annex-a-gen.tex",
f"\\newcommand{{\\wpAnnexGen}}{{\n{rows}}}\n")
# PLEN TDs
lines = []
for row in plen_rows:
q_display = _get_q_display(row)
name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/P")
source = make_href(URL + row.source.link, _format_source(row.source.name))
lines.append(table_row_str([q_display, name, source, escape_latex(row.title), q_display]))
rows = "".join(lines) if lines else "NONE"
write_result(RESULTS_DIR, "annex-a-plen.tex",
f"\\newcommand{{\\wpAnnexPlen}}{{\n{rows}}}\n")
# WP TDs
lines = []
for row in wp_rows:
q_display = _get_q_display(row)
name = make_href(URL + row.number.link, f"TD{row.number.value}{row.lastRev}/{wp_number}")
source = make_href(URL + row.source.link, _format_source(row.source.name))
lines.append(table_row_str([q_display, name, source, escape_latex(row.title), q_display]))
rows = "".join(lines) if lines else "NONE"
write_result(RESULTS_DIR, "annex-a-wp.tex",
f"\\newcommand{{\\wpAnnexWp}}{{\n{rows}}}\n")
def _gen_annex_b_reflectors(group, question_numbers):
"""Annex B: email reflector list for all questions in the WP."""
lines = []
for q in question_numbers:
reflector = f"q{q}sg{group}@lists.itu.int"
lines.append(f"\\item Q{q}/{group}: \\href{{mailto:{reflector}}}{{{reflector}}}")
content = "\n".join(lines) if lines else "\\item None"
write_result(RESULTS_DIR, "annex-b-reflectors.tex",
f"\\newcommand{{\\wpReflectorList}}{{\\begin{{itemize}}\n{content}\n\\end{{itemize}}}}\n")
def _gen_annex_c_new_work_items(wp_number, wp_rows):
"""Annex C: list of new work items with their justifications."""
lines = []
for row in wp_rows:
if is_new_work_item(row.title):
work_item, text_title = extract_new_work_item_info(row.title, wp_rows)
if not work_item:
continue
td_link = make_href(URL + row.number.link,
f"TD {row.number.value.replace(' ', '')}/{wp_number}")
display = f"{escape_latex(work_item)}"
if text_title:
display += f" -- {escape_latex(text_title)}"
lines.append(f"\\item {display} (see {td_link})")
if lines:
content = f"\\newcommand{{\\wpAnnexNewWorkItems}}{{\n" + "\n".join(lines) + "\n}\n"
else:
content = "% No new work items\n"
write_result(RESULTS_DIR, "annex-c-new-work-items.tex", content)
# ===================================================================
def _parse_liaison_destinations(title):
"""Parse '[to X]' and '[for info to Y]' from a liaison statement title.
Returns (action_to, info_to) strings.
"""
action_to = ""
info_to = ""
idx = 0
while idx < len(title):
start = title.find('[', idx)
if start < 0:
break
end = title.find(']', start)
if end < 0:
break
bracket = title[start + 1:end].strip()
if bracket.lower().startswith('for info'):
prefix = 'for info to '
pos = bracket.lower().find(prefix)
if pos >= 0:
info_to = bracket[pos + len(prefix):]
elif bracket.lower().startswith('to '):
action_to = bracket[3:]
idx = end + 1
return action_to, info_to
if __name__ == "__main__":
main()