1#!/usr/bin/env python3 2""" 3Upload CHANGES.md to Tidelift as Markdown chunks 4 5Put your Tidelift API token in a file called tidelift.token alongside this 6program, for example: 7 8 user/n3IwOpxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxc2ZwE4 9 10Run with two arguments: the .md file to parse, and the Tidelift package name: 11 12 python upload_relnotes.py CHANGES.md pypi/coverage 13 14Every section that has something that looks like a version number in it will 15be uploaded as the release notes for that version. 16 17""" 18 19import os.path 20import re 21import sys 22 23import requests 24 25class TextChunkBuffer: 26 """Hold onto text chunks until needed.""" 27 def __init__(self): 28 self.buffer = [] 29 30 def append(self, text): 31 """Add `text` to the buffer.""" 32 self.buffer.append(text) 33 34 def clear(self): 35 """Clear the buffer.""" 36 self.buffer = [] 37 38 def flush(self): 39 """Produce a ("text", text) tuple if there's anything here.""" 40 buffered = "".join(self.buffer).strip() 41 if buffered: 42 yield ("text", buffered) 43 self.clear() 44 45 46def parse_md(lines): 47 """Parse markdown lines, producing (type, text) chunks.""" 48 buffer = TextChunkBuffer() 49 50 for line in lines: 51 header_match = re.search(r"^(#+) (.+)$", line) 52 is_header = bool(header_match) 53 if is_header: 54 yield from buffer.flush() 55 hashes, text = header_match.groups() 56 yield (f"h{len(hashes)}", text) 57 else: 58 buffer.append(line) 59 60 yield from buffer.flush() 61 62 63def sections(parsed_data): 64 """Convert a stream of parsed tokens into sections with text and notes. 65 66 Yields a stream of: 67 ('h-level', 'header text', 'text') 68 69 """ 70 header = None 71 text = [] 72 for ttype, ttext in parsed_data: 73 if ttype.startswith('h'): 74 if header: 75 yield (*header, "\n".join(text)) 76 text = [] 77 header = (ttype, ttext) 78 elif ttype == "text": 79 text.append(ttext) 80 else: 81 raise Exception(f"Don't know ttype {ttype!r}") 82 yield (*header, "\n".join(text)) 83 84 85def relnotes(mdlines): 86 r"""Yield (version, text) pairs from markdown lines. 87 88 Each tuple is a separate version mentioned in the release notes. 89 90 A version is any section with \d\.\d in the header text. 91 92 """ 93 for _, htext, text in sections(parse_md(mdlines)): 94 m_version = re.search(r"\d+\.\d[^ ]*", htext) 95 if m_version: 96 version = m_version.group() 97 yield version, text 98 99def update_release_note(package, version, text): 100 """Update the release notes for one version of a package.""" 101 url = f"https://api.tidelift.com/external-api/lifting/{package}/release-notes/{version}" 102 token_file = os.path.join(os.path.dirname(__file__), "tidelift.token") 103 with open(token_file) as ftoken: 104 token = ftoken.read().strip() 105 headers = { 106 "Authorization": f"Bearer: {token}", 107 } 108 req_args = dict(url=url, data=text.encode('utf8'), headers=headers) 109 result = requests.post(**req_args) 110 if result.status_code == 409: 111 result = requests.put(**req_args) 112 print(f"{version}: {result.status_code}") 113 114def parse_and_upload(md_filename, package): 115 """Main function: parse markdown and upload to Tidelift.""" 116 with open(md_filename) as f: 117 markdown = f.read() 118 for version, text in relnotes(markdown.splitlines(True)): 119 update_release_note(package, version, text) 120 121if __name__ == "__main__": 122 parse_and_upload(*sys.argv[1:]) # pylint: disable=no-value-for-parameter 123