#hide
# default_exp core
API for auto-generated tagged releases, and release notes (from GitHub issues)
#export
from datetime import datetime
from textwrap import fill
from urllib.request import Request,urlopen
from urllib.error import HTTPError
from urllib.parse import urlencode
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
from configparser import ConfigParser
import os,shutil,json,subprocess
from fastrelease.fastscript import *
#hide
from nbdev.showdoc import show_doc
#export
GH_HOST = "https://api.github.com"
#export
def _issue_txt(issue):
res = '- {} ([#{}]({}))\n'.format(issue["title"].strip(), issue["number"], issue["url"])
body = issue['body']
if not body: return res
return res + fill(body.strip(), initial_indent=" - ", subsequent_indent=" ") + "\n"
def _issues_txt(iss, label):
if not iss: return ''
res = f"### {label}\n\n"
return res + '\n'.join(map(_issue_txt, iss))
def _config(cfg_name="settings.ini"):
cfg_path = Path().absolute()
while cfg_path != cfg_path.parent and not (cfg_path/cfg_name).exists(): cfg_path = cfg_path.parent
config_file = cfg_path/cfg_name
assert config_file.exists(), f"Couldn't find {cfg_name}"
config = ConfigParser()
config.read(config_file)
return config['DEFAULT'],cfg_path
#export
def run_proc(*args):
res = subprocess.run(args, capture_output=True)
if res.returncode: raise IOError(subprocess.stdout + ";;" + subprocess.stderr)
return res.stdout
#export
def do_request(url, post=False, headers=None, **data):
"Call GET or json-encoded POST on `url`, depending on `post`"
if data:
if post: data = json.dumps(data).encode('ascii')
else:
url += "?" + urlencode(data)
data = None
with urlopen(Request(url, headers=headers, data=data or None)) as res: return json.loads(res.read())
#export
class FastRelease:
def __init__(self, owner=None, repo=None, token=None, **groups):
"Create CHANGELOG.md from GitHub issues"
if not groups: groups = dict(breaking="Breaking Changes", enhancement="New Features", bug="Bugs Squashed")
self.cfg,cfg_path = _config()
os.chdir(cfg_path)
if not owner: owner = self.cfg['user']
if not repo: repo = self.cfg['lib_name']
if not token:
assert Path('token').exists, "Failed to find token"
token = Path('token').read_text().strip()
self.owner,self.repo,self.token,self.groups = owner,repo,token,groups
self.headers = { 'Authorization' : 'token ' + token }
self.repo_url = f"{GH_HOST}/repos/{owner}/{repo}"
def gh(self, path, complete=False, post=False, **data):
"Call GitHub API `path`"
if not complete: path = f"{self.repo_url}/{path}"
return do_request(path, headers=self.headers, post=post, **data)
def _tag_date(self, tag):
try: tag_d = self.gh(f"git/ref/tags/{tag}")
except HTTPError: raise Exception(f"Failed to find tag {tag}")
commit_d = self.gh(tag_d["object"]["url"], complete=True)
self.commit_date = commit_d["committer"]["date"]
return self.commit_date
def _issues(self, label):
return self.gh("issues", state='closed', sort='created', filter='all',
since=self.commit_date, labels=label)
def _issue_groups(self):
with ProcessPoolExecutor() as ex: return ex.map(self._issues, self.groups.keys())
def latest_release(self):
"Tag for the latest release"
return self.gh("releases/latest")["tag_name"]
def changelog(self, debug=False):
"Create the CHANGELOG.md file, or return the proposed text if `debug` is `True`"
fn = Path('CHANGELOG.md')
if not fn.exists(): fn.write_text("# Release notes\n\n<!-- do not remove -->\n")
txt = fn.read_text()
marker = '<!-- do not remove -->\n'
try:
latest = self.latest_release()
self._tag_date(latest)
except HTTPError: # no prior releases
self.commit_date = '2000-01-01T00:00:004Z'
res = f"\n## {self.cfg['version']}\n"
issues = self._issue_groups()
res += '\n'.join(_issues_txt(*o) for o in zip(issues, self.groups.values()))
res = txt.replace(marker, marker+res+"\n")
if debug: return res
shutil.copy(fn, fn.with_suffix(".bak"))
Path(fn).write_text(res)
def release(self):
"Tag and create a release in GitHub for the current version"
ver = self.cfg['version']
run_proc('git', 'tag', ver)
run_proc('git', 'push', '--tags')
run_proc('git', 'pull', '--tags')
self.gh("releases", post=True, tag_name=ver, name=ver, body=ver)
To create a markdown changelog, first create a FastRelease
object, passing a mapping from GitHub labels to markdown titles. Put your github token in a file named token
at the root of your repo.
show_doc(FastRelease.changelog)
FastRelease.changelog
[source]
FastRelease.changelog
(debug
=False
)
Create the CHANGELOG.md file, or return the proposed text if debug
is True
The default mapping from labels to markdown headings is:
# rel = FastRelease()
# print(rel.changelog(debug=True))
show_doc(FastRelease.release)
FastRelease.release
[source]
FastRelease.release
()
Tag and create a release in GitHub for the current version
This uses the version information from your settings.ini
.
show_doc(FastRelease.latest_release)
show_doc(FastRelease.gh)
#export
@call_parse
def fastrelease_changelog():
"Create a CHANGELOG.md file from closed and labeled GitHub issues"
FastRelease().changelog()
#export
@call_parse
def fastrelease_release():
"Tag and create a release in GitHub for the current version"
FastRelease().release()
#hide
from nbdev.export import notebook2script
notebook2script()
Converted 00_core.ipynb. Converted index.ipynb.