Mercurial > repos > shellac > sam_consensus_v3
comparison env/lib/python3.9/site-packages/planemo/github_util.py @ 0:4f3585e2f14b draft default tip
"planemo upload commit 60cee0fc7c0cda8592644e1aad72851dec82c959"
| author | shellac |
|---|---|
| date | Mon, 22 Mar 2021 18:12:50 +0000 |
| parents | |
| children |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 0:4f3585e2f14b |
|---|---|
| 1 """Utilities for interacting with Github.""" | |
| 2 from __future__ import absolute_import | |
| 3 | |
| 4 import io | |
| 5 import os | |
| 6 import stat | |
| 7 import tarfile | |
| 8 import tempfile | |
| 9 from distutils.dir_util import copy_tree | |
| 10 from pathlib import Path | |
| 11 | |
| 12 import requests | |
| 13 | |
| 14 from planemo import git | |
| 15 from planemo.io import ( | |
| 16 communicate, | |
| 17 IS_OS_X, | |
| 18 ) | |
| 19 | |
| 20 try: | |
| 21 import github | |
| 22 has_github_lib = True | |
| 23 except ImportError: | |
| 24 github = None | |
| 25 has_github_lib = False | |
| 26 | |
| 27 GH_VERSION = "1.5.0" | |
| 28 | |
| 29 NO_GITHUB_DEP_ERROR = ("Cannot use github functionality - " | |
| 30 "PyGithub library not available.") | |
| 31 FAILED_TO_DOWNLOAD_GH = "No gh executable available and it could not be installed." | |
| 32 DEFAULT_REMOTE_NAME = 'planemo-remote' | |
| 33 | |
| 34 | |
| 35 def get_github_config(ctx, allow_anonymous=False): | |
| 36 """Return a :class:`planemo.github_util.GithubConfig` for given configuration.""" | |
| 37 global_github_config = _get_raw_github_config(ctx) | |
| 38 return GithubConfig(global_github_config, allow_anonymous=allow_anonymous) | |
| 39 | |
| 40 | |
| 41 def clone_fork_branch(ctx, target, path, remote_name=DEFAULT_REMOTE_NAME, **kwds): | |
| 42 """Clone, fork, and branch a repository ahead of building a pull request.""" | |
| 43 git.checkout( | |
| 44 ctx, | |
| 45 target, | |
| 46 path, | |
| 47 branch=kwds.get("branch", None), | |
| 48 remote="origin", | |
| 49 from_branch="master" | |
| 50 ) | |
| 51 if kwds.get("fork"): | |
| 52 try: | |
| 53 fork(ctx, path, remote_name=remote_name, **kwds) | |
| 54 except Exception: | |
| 55 pass | |
| 56 if 'GITHUB_USER' in os.environ: | |
| 57 # On CI systems fork doesn't add a local remote under circumstances I don't quite understand, | |
| 58 # but that's probably linked to https://github.com/cli/cli/issues/2722 | |
| 59 cmd = ['git', 'remote', 'add', remote_name, f"https://github.com/{os.environ['GITHUB_USER']}/{os.path.basename(target)}"] | |
| 60 try: | |
| 61 communicate(cmd, cwd=path) | |
| 62 except RuntimeError: | |
| 63 # Can add the remote only once | |
| 64 pass | |
| 65 return remote_name | |
| 66 | |
| 67 | |
| 68 def fork(ctx, path, remote_name=DEFAULT_REMOTE_NAME, **kwds): | |
| 69 """Fork the target repository using ``gh``.""" | |
| 70 gh_path = ensure_gh(ctx, **kwds) | |
| 71 gh_env = get_gh_env(ctx, path, **kwds) | |
| 72 cmd = [gh_path, "repo", "fork", '--remote=true', '--remote-name', remote_name] | |
| 73 communicate(cmd, cwd=path, env=gh_env) | |
| 74 return remote_name | |
| 75 | |
| 76 | |
| 77 def get_or_create_repository(ctx, owner, repo, dry_run=True, **kwds): | |
| 78 """Clones or creates a repository and returns path on disk""" | |
| 79 target = os.path.realpath(tempfile.mkdtemp()) | |
| 80 remote_repo = "https://github.com/{owner}/{repo}".format(owner=owner, repo=repo) | |
| 81 try: | |
| 82 ctx.log('Cloning {}'.format(remote_repo)) | |
| 83 git.clone(ctx, src=remote_repo, dest=target) | |
| 84 except Exception: | |
| 85 ctx.log('Creating repository {}'.format(remote_repo)) | |
| 86 target = create_repository(ctx, owner=owner, repo=repo, dest=target, dry_run=dry_run) | |
| 87 return target | |
| 88 | |
| 89 | |
| 90 def create_repository(ctx, owner, repo, dest, dry_run, **kwds): | |
| 91 gh_path = ensure_gh(ctx, **kwds) | |
| 92 gh_env = get_gh_env(ctx, dry_run=dry_run, **kwds) | |
| 93 cmd = [gh_path, 'repo', 'create', '-y', '--public', "{owner}/{repo}".format(owner=owner, repo=repo)] | |
| 94 if dry_run: | |
| 95 "Would run command '{}'".format(" ".join(cmd)) | |
| 96 git.init(ctx, dest) | |
| 97 return dest | |
| 98 communicate(cmd, env=gh_env, cwd=dest) | |
| 99 return os.path.join(dest, repo) | |
| 100 | |
| 101 | |
| 102 def rm_dir_contents(directory, ignore_dirs=(".git")): | |
| 103 directory = Path(directory) | |
| 104 for item in directory.iterdir(): | |
| 105 if item.name not in ignore_dirs: | |
| 106 if item.is_dir(): | |
| 107 rm_dir_contents(item) | |
| 108 else: | |
| 109 item.unlink() | |
| 110 | |
| 111 | |
| 112 def add_dir_contents_to_repo(ctx, from_dir, target_dir, target_repository_path, version, dry_run, notes=""): | |
| 113 ctx.log("From {} to {}".format(from_dir, target_repository_path)) | |
| 114 rm_dir_contents(target_repository_path) | |
| 115 copy_tree(from_dir, target_repository_path) | |
| 116 git.add(ctx, target_repository_path, target_repository_path) | |
| 117 message = "Update for version {version}".format(version=version) | |
| 118 if notes: | |
| 119 message += "\n{notes}".format(notes=notes) | |
| 120 git.commit(ctx, repo_path=target_repository_path, message=message) | |
| 121 if not dry_run: | |
| 122 git.push(ctx, target_repository_path) | |
| 123 | |
| 124 | |
| 125 def assert_new_version(ctx, version, owner, repo): | |
| 126 remote_repo = "https://github.com/{owner}/{repo}".format(owner=owner, repo=repo) | |
| 127 try: | |
| 128 tags_and_versions = git.ls_remote(ctx, remote_repo=remote_repo) | |
| 129 if "refs/tags/v{}".format(version) in tags_and_versions or "refs/tags/{}".format(version) in tags_and_versions: | |
| 130 raise Exception("Version '{}' for {}/{} exists already. Please change the version.".format(version, owner, repo)) | |
| 131 except RuntimeError: | |
| 132 # repo doesn't exist | |
| 133 pass | |
| 134 | |
| 135 | |
| 136 def changelog_in_repo(target_repository_path): | |
| 137 changelog = [] | |
| 138 for path in os.listdir(target_repository_path): | |
| 139 if 'changelog.md' in path.lower(): | |
| 140 header_seen = False | |
| 141 header_chars = ('---', '===', '~~~') | |
| 142 with(open(os.path.join(target_repository_path, path))) as changelog_fh: | |
| 143 for line in changelog_fh: | |
| 144 if line.startswith(header_chars): | |
| 145 if header_seen: | |
| 146 return "\n".join(changelog[:-1]) | |
| 147 else: | |
| 148 header_seen = True | |
| 149 return "\n".join(changelog) | |
| 150 | |
| 151 | |
| 152 def create_release(ctx, from_dir, target_dir, owner, repo, version, dry_run, notes="", **kwds): | |
| 153 assert_new_version(ctx, version, owner=owner, repo=repo) | |
| 154 target_repository_path = get_or_create_repository(ctx, owner=owner, repo=repo, dry_run=dry_run) | |
| 155 add_dir_contents_to_repo(ctx, from_dir, target_dir, target_repository_path, version=version, dry_run=dry_run, notes=notes) | |
| 156 gh_path = ensure_gh(ctx, **kwds) | |
| 157 gh_env = get_gh_env(ctx, dry_run=dry_run, **kwds) | |
| 158 cmd = [ | |
| 159 gh_path, | |
| 160 'release', | |
| 161 '-R', | |
| 162 "{}/{}".format(owner, repo), | |
| 163 'create', | |
| 164 "v{version}".format(version=version), | |
| 165 '--title', | |
| 166 str(version), | |
| 167 ] | |
| 168 cmd.extend(['--notes', notes or changelog_in_repo(target_repository_path)]) | |
| 169 if not dry_run: | |
| 170 communicate(cmd, env=gh_env) | |
| 171 else: | |
| 172 ctx.log("Would run command '{}'".format(" ".join(cmd))) | |
| 173 | |
| 174 | |
| 175 def pull_request(ctx, path, message=None, repo=None, **kwds): | |
| 176 """Create a pull request against the origin of the path using ``gh``.""" | |
| 177 gh_path = ensure_gh(ctx, **kwds) | |
| 178 gh_env = get_gh_env(ctx, path, **kwds) | |
| 179 cmd = [gh_path, "pr", "create"] | |
| 180 if message is None: | |
| 181 cmd.append('--fill') | |
| 182 else: | |
| 183 lines = message.splitlines() | |
| 184 cmd.extend(['--title', lines[0]]) | |
| 185 if len(lines) > 1: | |
| 186 cmd.extend(["--body", "\n".join(lines[1:])]) | |
| 187 if repo: | |
| 188 cmd.extend(['--repo', repo]) | |
| 189 communicate(cmd, env=gh_env) | |
| 190 | |
| 191 | |
| 192 def get_gh_env(ctx, path=None, dry_run=False, **kwds): | |
| 193 """Return a environment dictionary to run gh with given user and repository target.""" | |
| 194 if path is None: | |
| 195 env = {} | |
| 196 else: | |
| 197 env = git.git_env_for(path).copy() | |
| 198 if not dry_run: | |
| 199 github_config = _get_raw_github_config(ctx) | |
| 200 if github_config is not None: | |
| 201 if "access_token" in github_config: | |
| 202 env["GITHUB_TOKEN"] = github_config["access_token"] | |
| 203 | |
| 204 return env | |
| 205 | |
| 206 | |
| 207 def ensure_gh(ctx, **kwds): | |
| 208 """Ensure gh is available for planemo | |
| 209 | |
| 210 This method will ensure ``gh`` is installed at the correct version. | |
| 211 | |
| 212 For more information on ``gh`` checkout https://cli.github.com/ | |
| 213 """ | |
| 214 planemo_gh_path = os.path.join(ctx.workspace, f"gh-{GH_VERSION}") | |
| 215 if not os.path.exists(planemo_gh_path): | |
| 216 _try_download_gh(planemo_gh_path) | |
| 217 | |
| 218 if not os.path.exists(planemo_gh_path): | |
| 219 raise Exception(FAILED_TO_DOWNLOAD_GH) | |
| 220 | |
| 221 return planemo_gh_path | |
| 222 | |
| 223 | |
| 224 def _try_download_gh(planemo_gh_path): | |
| 225 link = _gh_link() | |
| 226 path = Path(planemo_gh_path) | |
| 227 resp = requests.get(link) | |
| 228 with tarfile.open(fileobj=io.BytesIO(resp.content)) as tf, path.open('wb') as outfile: | |
| 229 for member in tf.getmembers(): | |
| 230 if member.name.endswith('bin/gh'): | |
| 231 outfile.write(tf.extractfile(member).read()) | |
| 232 path.chmod(path.stat().st_mode | stat.S_IEXEC) | |
| 233 | |
| 234 | |
| 235 def _get_raw_github_config(ctx): | |
| 236 """Return a :class:`planemo.github_util.GithubConfig` for given configuration.""" | |
| 237 if "github" not in ctx.global_config: | |
| 238 if "GITHUB_TOKEN" in os.environ: | |
| 239 return { | |
| 240 "access_token": os.environ["GITHUB_TOKEN"], | |
| 241 } | |
| 242 if "github" not in ctx.global_config: | |
| 243 raise Exception("github account not found in planemo config and GITHUB_TOKEN environment variables unset") | |
| 244 return ctx.global_config["github"] | |
| 245 | |
| 246 | |
| 247 class GithubConfig(object): | |
| 248 """Abstraction around a Github account. | |
| 249 | |
| 250 Required to use ``github`` module methods that require authorization. | |
| 251 """ | |
| 252 | |
| 253 def __init__(self, config, allow_anonymous=False): | |
| 254 if not has_github_lib: | |
| 255 raise Exception(NO_GITHUB_DEP_ERROR) | |
| 256 if "access_token" not in config: | |
| 257 if not allow_anonymous: | |
| 258 raise Exception("github authentication unavailable") | |
| 259 github_object = github.Github() | |
| 260 else: | |
| 261 github_object = github.Github(config["access_token"]) | |
| 262 self._github = github_object | |
| 263 | |
| 264 | |
| 265 def _gh_link(): | |
| 266 if IS_OS_X: | |
| 267 template_link = "https://github.com/cli/cli/releases/download/v%s/gh_%s_macOS_amd64.tar.gz" | |
| 268 else: | |
| 269 template_link = "https://github.com/cli/cli/releases/download/v%s/gh_%s_linux_amd64.tar.gz" | |
| 270 return template_link % (GH_VERSION, GH_VERSION) | |
| 271 | |
| 272 | |
| 273 def publish_as_gist_file(ctx, path, name="index"): | |
| 274 """Publish a gist. | |
| 275 | |
| 276 More information on gists at http://gist.github.com/. | |
| 277 """ | |
| 278 github_config = get_github_config(ctx, allow_anonymous=False) | |
| 279 user = github_config._github.get_user() | |
| 280 with open(path, "r") as fh: | |
| 281 content = fh.read() | |
| 282 content_file = github.InputFileContent(content) | |
| 283 gist = user.create_gist(False, {name: content_file}) | |
| 284 return gist.files[name].raw_url | |
| 285 | |
| 286 | |
| 287 def get_repository_object(ctx, name): | |
| 288 github_object = get_github_config(ctx, allow_anonymous=True) | |
| 289 return github_object._github.get_repo(name) | |
| 290 | |
| 291 | |
| 292 __all__ = ( | |
| 293 "add_dir_contents_to_repo", | |
| 294 "clone_fork_branch", | |
| 295 "create_release", | |
| 296 "ensure_gh", | |
| 297 "fork", | |
| 298 "get_github_config", | |
| 299 "get_gh_env", | |
| 300 "get_or_create_repository", | |
| 301 "publish_as_gist_file", | |
| 302 ) |
