|
| 1 | +#!/usr/bin/env -S uv run --script --quiet |
| 2 | + |
| 3 | +# /// script |
| 4 | +# requires-python = ">=3.12" |
| 5 | +# dependencies = [ |
| 6 | +# "httpx", |
| 7 | +# "libcst", |
| 8 | +# "lxml", |
| 9 | +# ] |
| 10 | +# /// |
| 11 | + |
| 12 | +import re |
| 13 | +import sys |
| 14 | +from pathlib import Path |
| 15 | + |
| 16 | +import httpx |
| 17 | +import libcst as cst |
| 18 | +from lxml import etree |
| 19 | + |
| 20 | +if len(sys.argv) != 2: |
| 21 | + sys.exit(f"Usage: {sys.argv[0]} VERSION") |
| 22 | + |
| 23 | +version = sys.argv[1] |
| 24 | + |
| 25 | +VERSION_RE = re.compile(r"^(?:saas[~-])?([0-9]+)(?:\.([0-9]+))?$") |
| 26 | + |
| 27 | +if (match := VERSION_RE.match(version)) is None: |
| 28 | + sys.exit(f"Invalid version: {version!r}") |
| 29 | + |
| 30 | +major, minor = match.groups(default="0") |
| 31 | + |
| 32 | +version_url = major if minor == "0" else f"{major}-{minor}" |
| 33 | +full_version = f"{major}.0" if minor == "0" else f"saas~{major}.{minor}" |
| 34 | + |
| 35 | +html = httpx.get(f"https://www.odoo.com/odoo-{version_url}-release-notes") |
| 36 | +if html.status_code != 200: |
| 37 | + sys.exit(f"Cannot fetch release notes page for version {version}") |
| 38 | + |
| 39 | +root = etree.fromstring(html.text, parser=etree.HTMLParser()) |
| 40 | +iframe = root.xpath("//main//iframe[contains(@src, 'youtube.com') or contains(@src, 'youtube-nocookie.com')]") |
| 41 | +if not iframe: |
| 42 | + sys.exit(f"Cannot find youtube video in {html.url}") |
| 43 | + |
| 44 | +yt_link = httpx.URL(iframe[0].attrib["src"]) |
| 45 | +video_id = yt_link.path.removeprefix("/embed/") |
| 46 | + |
| 47 | + |
| 48 | +report_py = Path(__file__).parent.parent / "src" / "util" / "report.py" |
| 49 | + |
| 50 | +source_tree = cst.parse_module(report_py.read_bytes()) |
| 51 | + |
| 52 | + |
| 53 | +class Transformer(cst.CSTTransformer): |
| 54 | + def __init__(self): |
| 55 | + self.video_dict = None |
| 56 | + self.key_found = False |
| 57 | + super().__init__() |
| 58 | + |
| 59 | + def visit_Assign(self, node): |
| 60 | + match node: |
| 61 | + case cst.Assign( |
| 62 | + targets=[cst.AssignTarget(target=cst.Name(value="ODOO_SHOWCASE_VIDEOS"))], |
| 63 | + value=video_dict, |
| 64 | + ): |
| 65 | + self.video_dict = video_dict |
| 66 | + return True |
| 67 | + return False |
| 68 | + |
| 69 | + def visit_Dict(self, node): |
| 70 | + return node is self.video_dict |
| 71 | + |
| 72 | + def leave_DictElement(self, original_node, updated_node): |
| 73 | + if original_node.key.raw_value == full_version: |
| 74 | + self.key_found = True |
| 75 | + if original_node.value.raw_value != video_id: |
| 76 | + updated_node = updated_node.with_changes(value=cst.SimpleString(f'"{video_id}"')) |
| 77 | + return updated_node |
| 78 | + |
| 79 | + def leave_Dict(self, original_node, updated_node): |
| 80 | + if original_node is self.video_dict: |
| 81 | + if self.key_found: |
| 82 | + elements = updated_node.elements |
| 83 | + else: |
| 84 | + new_elem = updated_node.elements[0].with_changes( |
| 85 | + key=cst.SimpleString(f'"{full_version}"'), value=cst.SimpleString(f'"{video_id}"') |
| 86 | + ) |
| 87 | + elements = [new_elem, *updated_node.elements] |
| 88 | + |
| 89 | + elements = sorted(elements, reverse=True, key=lambda e: VERSION_RE.match(e.key.raw_value).groups("0")) |
| 90 | + updated_node = updated_node.with_changes(elements=elements) |
| 91 | + return updated_node |
| 92 | + |
| 93 | + |
| 94 | +modified_tree = source_tree.visit(Transformer()) |
| 95 | + |
| 96 | +report_py.write_text(modified_tree.code) |
0 commit comments