From 9df7275429903b38120aca7b0c9b3d7726da48aa Mon Sep 17 00:00:00 2001 From: Aditya Toshniwal Date: Wed, 30 Apr 2025 15:31:20 +0530 Subject: [PATCH] Add a python script to parse a release note file and convert to Email, Markdown and HTML formats required for pgAdmin release --- tools/release_converter.py | 368 +++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 tools/release_converter.py diff --git a/tools/release_converter.py b/tools/release_converter.py new file mode 100644 index 000000000..c8345bea3 --- /dev/null +++ b/tools/release_converter.py @@ -0,0 +1,368 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2025, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +""" +Parses a pgAdmin release note file in ReStructuredText (RST) format +and converts it into Email (HTML), Markdown, and HTML (web) formats. +Issue links are always omitted from lists in all outputs. +Allows skipping specific issue numbers via command line. +The Email HTML output can optionally be saved to a file. + +Usage: + python release_converter.py \\ + [--output-email-html ] \\ + [--skip-issues ISSUE_NUM [ISSUE_NUM ...]] + +Examples: + # Default: No issue links, don't skip issues, print all to console + python release_converter.py path/to/notes.rst + + # Skip issues 8602 and 8603 + python release_converter.py path/to/notes.rst --skip-issues 8602 8603 + + # Save email output, default skip behavior + python release_converter.py path/to/notes.rst --output-email-html email.html +""" + +import re +import argparse +import sys +import html + + +# --- PARSING FUNCTION (parse_rst_release_note) --- +def parse_rst_release_note(rst_text): + """Parses the RST release note text to extract key information.""" + data = { + 'version': None, + 'release_date': None, + 'features': [], + 'bug_fixes': [], + 'housekeeping': [] + } + + version_match = re.search(r'Version\s+([^\n\*]+)', rst_text) + if version_match: + data['version'] = version_match.group(1).strip() + else: + print("Warning: Could not parse version.", file=sys.stderr) + + date_match = re.search(r'Release date:\s*(\d{4}-\d{2}-\d{2})', rst_text) + if date_match: + data['release_date'] = date_match.group(1).strip() + else: + print("Warning: Could not parse release date.", file=sys.stderr) + + current_section_list = None + lines = rst_text.splitlines() + + for i, line_raw in enumerate(lines): + line_stripped = line_raw.strip() + if i > 0: + prev_line_stripped = lines[i-1].strip() + if len(prev_line_stripped) > 3 and all(c == '*' for c in prev_line_stripped): + if i > 1: + header_text_line = lines[i-2].strip().lower() + if "new features" in header_text_line: + current_section_list = data['features'] + elif "bug fixes" in header_text_line: + current_section_list = data['bug_fixes'] + elif "housekeeping" in header_text_line: + current_section_list = data['housekeeping'] + else: + pass + continue + + if current_section_list is not None and line_stripped.startswith('|'): + line_to_parse = line_stripped + item_match = re.match(r'\|\s*`Issue\s+#(\d+)\s+<([^>]+)>`_\s*-\s*(.*)', line_to_parse) + if item_match: + issue_num, url, description = item_match.groups() + item_data = { + 'issue': issue_num.strip(), + 'url': url.strip(), + 'description': description.strip().rstrip('.') + } + current_section_list.append(item_data) + else: + simple_match = re.match(r'\|\s*(.*)', line_to_parse) + simple_text = simple_match.group(1).strip().rstrip('.') if simple_match else None + if simple_text: + item_data = { + 'issue': None, + 'url': None, + 'description': simple_text + } + current_section_list.append(item_data) + + data['bugs_housekeeping'] = data['bug_fixes'] + data['housekeeping'] + + if not data['features']: + print("Warning: No 'New features' items parsed.", file=sys.stderr) + if not data['bugs_housekeeping']: + print("Warning: No 'Bug fixes' or 'Housekeeping' items parsed.", file=sys.stderr) + + return data + + +# --- Helper for Plurals --- +def pluralize(count, singular, plural=None): + """Adds 's' for pluralization if count is not 1.""" + if count == 1: + return f"{count} {singular}" + else: + plural_form = plural if plural else singular + 's' + return f"{count} {plural_form}" + + +# --- Formatting Functions --- +def format_email_html(data, skip_issues_set=None): + """Formats the extracted data into HTML suitable for email (Google Doc style).""" + if skip_issues_set is None: + skip_issues_set = set() + if not data.get('version'): + return "

Error: Version not found in parsed data.

" + + version = data['version'] + release_url = f"https://www.pgadmin.org/docs/pgadmin4/{version}/release_notes_{version.replace('.', '_')}.html" + download_url = "https://www.pgadmin.org/download/" + website_url = "https://www.pgadmin.org/" + + filtered_features = [item for item in data.get('features', []) if item.get('issue') not in skip_issues_set] + filtered_bugs = [item for item in data.get('bugs_housekeeping', []) if item.get('issue') not in skip_issues_set] + num_features = len(filtered_features) + num_bugs_housekeeping = len(filtered_bugs) + + output = f"

pgAdmin 4 v{version} Released

\n" + output += f"

The pgAdmin Development Team is pleased to announce pgAdmin 4 version {version}.

\n" + output += (f"

This release of pgAdmin 4 includes {pluralize(num_features, 'new feature')} " + f"and {pluralize(num_bugs_housekeeping, 'bug fix', 'bug fixes')}/housekeeping change{'s' if num_bugs_housekeeping != 1 else ''}. ") + output += f'For more details please see the Release Notes.

\n' + output += '

pgAdmin is the leading Open Source graphical management tool for PostgreSQL. For more information, please see

\n' + output += f'

{website_url}

\n' + output += "

Notable changes in this release include:

\n" + + if filtered_features: + output += "

Features

\n
    \n" + for item in filtered_features: + desc = html.escape(item.get('description', 'N/A').strip()) + output += f"
  • {desc}.
  • \n" + output += "
\n" + + if filtered_bugs: + output += "

Bugs/Housekeeping

\n
    \n" + for item in filtered_bugs: + desc = html.escape(item.get('description', 'N/A').strip()) + output += f"
  • {desc}.
  • \n" + output += "
\n" + + output += "

Builds for Windows and macOS are available now, along with a Python Wheel,
" + output += "Docker Container, RPM, DEB Package, and source code tarball from:
" + output += f'{download_url}

\n' + output += "

--
Release Manager
pgAdmin Project

\n" + + return output + + +def format_markdown(data, skip_issues_set=None): + """Formats the extracted data into Markdown (no issue links in lists).""" + if skip_issues_set is None: + skip_issues_set = set() + if not data.get('version'): + return "Error: Version not found in parsed data." + + version = data['version'] + release_url = f"https://www.pgadmin.org/docs/pgadmin4/{version}/release_notes_{version.replace('.', '_')}.html" + download_url = "https://www.pgadmin.org/download/" + website_url = "https://www.pgadmin.org/" + + filtered_features = [item for item in data.get('features', []) if item.get('issue') not in skip_issues_set] + filtered_bugs = [item for item in data.get('bugs_housekeeping', []) if item.get('issue') not in skip_issues_set] + num_features = len(filtered_features) + num_bugs_housekeeping = len(filtered_bugs) + + output = f"The pgAdmin Development Team is pleased to announce pgAdmin 4 version {version}. " + output += (f"This release of pgAdmin 4 includes {pluralize(num_features, 'new feature')} " + f"and {pluralize(num_bugs_housekeeping, 'bug fix', 'bug fixes')}/housekeeping change{'s' if num_bugs_housekeeping != 1 else ''}. ") + output += f"For more details, please see the [release notes]({release_url}).\n \n" + output += f"pgAdmin is the leading Open Source graphical management tool for PostgreSQL. For more information, please see [the website]({website_url}).\n\n" + output += "Notable changes in this release include:\n \n" + + if filtered_features: + output += "### Features:\n" + for item in filtered_features: + desc = item.get('description', 'N/A').strip() + # Always omit the link + output += f"* {desc}.\n" + output += "\n" + + if filtered_bugs: + output += "### Bugs/Housekeeping:\n" + for item in filtered_bugs: + desc = item.get('description', 'N/A').strip() + # Always omit the link + output += f"* {desc}.\n" + output += "\n" + + output += f"Builds for Windows and macOS are available now, along with a Python Wheel, Docker Container, RPM, DEB Package, and source code tarball from the [download area]({download_url})." + + return output + + +def format_html(data, skip_issues_set=None): + """Formats the extracted data into HTML for web news articles (no issue links in lists).""" + if skip_issues_set is None: + skip_issues_set = set() + import html + + if not data.get('version'): + return "

Error: Version not found in parsed data.

" + + version = data['version'] + release_url = f"/docs/pgadmin4/{version}/release_notes_{version.replace('.', '_')}.html" + download_url = "/download" + + filtered_features = [item for item in data.get('features', []) if item.get('issue') not in skip_issues_set] + filtered_bugs = [item for item in data.get('bugs_housekeeping', []) if item.get('issue') not in skip_issues_set] + num_features = len(filtered_features) + num_bugs_housekeeping = len(filtered_bugs) + + output = f"

The pgAdmin Development Team is pleased to announce pgAdmin 4 version {version}. " + output += (f"This release of pgAdmin 4 includes {pluralize(num_features, 'new feature')} " + f"and {pluralize(num_bugs_housekeeping, 'bug fix', 'bug fixes')}/housekeeping change{'s' if num_bugs_housekeeping != 1 else ''}. ") + output += f'For more details, please see the release notes.

\n' + output += "

Notable changes in this release include:

\n" + + if filtered_features: + output += "

Features:

\n
    \n" + for item in filtered_features: + desc = html.escape(item.get('description', 'N/A').strip()) + # Always omit the link, keep bolding + output += f"
  • {desc}.
  • \n" + output += "
\n" + + if filtered_bugs: + output += "

Bugs/Housekeeping:

\n
    \n" + for item in filtered_bugs: + desc = html.escape(item.get('description', 'N/A').strip()) + # Always omit the link + output += f"
  • {desc}.
  • \n" + output += "
\n" + + output += f'

Download your copy now!

' + + return output + + +# --- Main Execution --- +if __name__ == "__main__": + # --- Setup Argument Parser --- + parser = argparse.ArgumentParser( + # ***MODIFIED: Updated description*** + description="Converts pgAdmin RST release notes to Email (HTML), Markdown, and HTML (web) formats.\n" + "Issue links are omitted from lists. Allows skipping specific issues.", + formatter_class=argparse.RawDescriptionHelpFormatter, + # ***MODIFIED: Updated examples*** + epilog="Examples:\n" + " # Default: Don't skip issues\n" + " python release_converter.py path/to/notes.rst\n\n" + " # Skip issues 8602 and 8603\n" + " python release_converter.py path/to/notes.rst --skip-issues 8602 8603\n\n" + " # Save email output, default skip behavior\n" + " python release_converter.py path/to/notes.rst --output-email-html email.html" + ) + parser.add_argument( + "input_file", + metavar="", + type=str, + help="Path to the input ReStructuredText (.rst) release note file." + ) + parser.add_argument( + "--output-email-html", + metavar="", + type=str, + default=None, + help="Optional path to save the Email HTML output to a file." + ) + parser.add_argument( + "--skip-issues", + metavar="ISSUE_NUM", + type=str, + nargs='+', + default=[], + help="List of issue numbers (e.g., 8602 8603) to skip from output lists." + ) + args = parser.parse_args() + + # --- Read Input File --- + input_rst_content = "" + try: + with open(args.input_file, "r", encoding="utf-8") as f: + input_rst_content = f.read() + print(f"Successfully read file: {args.input_file}", file=sys.stderr) + except FileNotFoundError: + print(f"Error: Input file not found at '{args.input_file}'", file=sys.stderr) + sys.exit(1) + except Exception as e: + print(f"Error reading file '{args.input_file}': {e}", file=sys.stderr) + sys.exit(1) + + # --- Parse the input data --- + print("Parsing release notes...", file=sys.stderr) + parsed_data = parse_rst_release_note(input_rst_content) + + if not parsed_data.get('version'): + print("\nError: Parsing failed to find version. Cannot proceed.", file=sys.stderr) + sys.exit(1) + + # --- Create skip set --- + skip_issues_set = set(args.skip_issues) + if skip_issues_set: + print(f"Attempting to skip issues: {', '.join(sorted(list(skip_issues_set)))}", file=sys.stderr) + + # --- Generate the different formats --- + print("Generating output formats...", file=sys.stderr) + # ***MODIFIED: Removed include_links from calls*** + email_html_output = format_email_html(parsed_data, skip_issues_set=skip_issues_set) + markdown_output = format_markdown(parsed_data, skip_issues_set=skip_issues_set) + news_html_output = format_html(parsed_data, skip_issues_set=skip_issues_set) + print("Format generation complete.", file=sys.stderr) + + # --- Handle Outputs --- + if args.output_email_html: + try: + output_filename = args.output_email_html + if not output_filename.lower().endswith(('.html', '.htm')): + print(f"Warning: Output file '{output_filename}' does not end with .html or .htm. The content is HTML.", file=sys.stderr) + + with open(output_filename, "w", encoding="utf-8") as f: + f.write(email_html_output) + print(f"Email HTML output successfully saved to: {output_filename}", file=sys.stderr) + except Exception as e: + print(f"Error writing Email HTML output to file '{args.output_email_html}': {e}", file=sys.stderr) + print("\n--- Email HTML Output ---") + print(email_html_output) + print("\n---------------------------------\n") + else: + print("\n--- Email HTML Output ---") + print(email_html_output) + print("\n---------------------------------\n") + + # --- Output Other Formats (still to console) --- + print("--- Markdown Output ---") + print(markdown_output) + print("\n---------------------------------\n") + + print("--- News Article HTML Output ---") + print(news_html_output) + print("\n---------------------------------\n") + + print("Script finished.", file=sys.stderr) \ No newline at end of file