From 0006d60ccfa697fefbd286de0ed4a1789e0de29d Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" Date: Sun, 15 Jul 2018 20:37:32 -0400 Subject: [PATCH] Add script that will configure a GitHub checkout for RTD. Fixes #2725. --- contrib/add-project.py | 168 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 contrib/add-project.py diff --git a/contrib/add-project.py b/contrib/add-project.py new file mode 100644 index 00000000000..f9926a1ac60 --- /dev/null +++ b/contrib/add-project.py @@ -0,0 +1,168 @@ +""" +Script to add a Git project as cloned from a Github repository +to ReadTheDocs. Invoke from the checkout. +""" + +__requires__ = ['requests-toolbelt', 'autocommand', 'keyring'] + + +import os +import getpass +import re +import urllib.parse +import subprocess +import pathlib + +import autocommand +import keyring +from requests_toolbelt import sessions + + +rtd = sessions.BaseUrlSession( + os.environ.get('RTD_URL', 'https://readthedocs.org/api/v2/')) +github = sessions.BaseUrlSession('https://api.github.com/') +github.headers.update(Accept='application/vnd.github.v3+json') + + +class User: + """ + A User (with a password) in RTD. + + Resolves username using ``getpass.getuser()``. Override with + RTD_USERNAME environment variable. + + Resolves password using keyring. Install keyring and run + ``keyring set https://readthedocs.org/ $USER`` to set the pw. + Override with RTD_PASSWORD environment variable. + """ + def __init__(self): + self.name = os.environ.get('RTD_USERNAME') or getpass.getuser() + system = rtd.create_url('/') + self.password = ( + os.environ.get('RTD_PASSWORD') + or keyring.get_password(system, self.name) + ) + + @property + def id(self): + resp = rtd.get('../v1/user/', params=dict(username=self.name)) + resp.raise_for_status() + ob, = resp.json()['objects'] + return ob['id'] + + @property + def tuple(self): + return self.name, self.password + + +class Sluggable(str): + """ + A name for use in RTD with a 'slug' version. + """ + @property + def slug(self): + return self.replace('.', '') + + +class Repo: + """ + A Git repo + """ + + def __init__(self, root): + self.root = root + cmd = ['git', '-C', root, 'remote', 'get-url', 'origin'] + proc = subprocess.run( + cmd, check=True, text=True, stdout=subprocess.PIPE) + self.url = proc.stdout.strip() + + @property + def name(self): + return Sluggable(pathlib.Path(self.url).stem) + + +def create_project(repo): + """ + Create the project from a Repo + """ + user = User() + payload = dict( + repo=repo.url, + slug=repo.name.slug, + name=repo.name, + users=[user.id], + ) + resp = rtd.post('project/', json=payload, auth=user.tuple) + resp.raise_for_status() + + +def configure_github(name, url): + """ + Given a project name and webhook URL, configure the webhook + in GitHub. + + Resolves username from ``getpass.getuser()``. Override with + ``GITHUB_USERNAME``. + + Resolves access token from keyring for username and system + 'github.com'. Override with ``GITHUB_TOKEN`` environment + variable. + """ + user = os.environ.get('GITHUB_USERNAME') or getpass.getuser() + token = ( + keyring.get_password('github.com', user) + or os.environ['GITHUB_TOKEN'] + ) + headers = dict(Authorization=f'token {token}') + path = f'/repos/{user}/{name}/hooks' + params = dict( + name='web', + config=dict( + url=url, + content_type='json', + ), + ) + github.post(path, json=params, headers=headers) + + +def configure_webhook(name): + """ + Identify the webhook URL for a RTD project named name. + """ + login_path = '/accounts/login/' + resp = rtd.get(login_path) + token = rtd.cookies.get('csrftoken') or rtd.cookies['csrf'] + user = User() + params = dict( + login=user.name, + password=user.password, + csrfmiddlewaretoken=token, + next='/', + ) + headers = dict(Referer=rtd.create_url(login_path)) + resp = rtd.post(login_path, data=params, headers=headers) + token = rtd.cookies.get('csrftoken') or rtd.cookies['csrf'] + params = dict( + integration_type='github_webhook', + csrfmiddlewaretoken=token, + next='/', + ) + create_path = f'/dashboard/{name.slug}/integrations/create/' + headers = dict( + Referer=rtd.create_url(create_path), + ) + resp = rtd.post( + create_path, + data=params, + headers=headers, + ) + resp.raise_for_status() + ref = re.search(f'.*?webhook.*?', resp.text).group(1) + return urllib.parse.urljoin(resp.url, ref) + + +@autocommand.autocommand(__name__) +def main(repo: Repo = Repo('.')): + create_project(repo) + url = configure_webhook(repo.name) + configure_github(repo.name, url)