Beispiel #1
0
def test_patch_with_data(mocker):
    patch = mocker.patch("getgist.request.requests.patch")
    requests = GetGistRequests({"foo": "bar"})
    requests.patch("foobar", data=42, bar="foo")
    patch.assert_called_once_with("foobar",
                                  data=42,
                                  headers={
                                      "foo": "bar",
                                      "bar": "foo"
                                  })
Beispiel #2
0
def test_post_without_data(mocker):
    post = mocker.patch("getgist.request.requests.post")
    requests = GetGistRequests({"foo": "bar"})
    requests.post("foobar", bar="foo")
    post.assert_called_once_with("foobar",
                                 data=None,
                                 headers={
                                     "foo": "bar",
                                     "bar": "foo"
                                 })
Beispiel #3
0
def test_get_with_params(mocker):
    get = mocker.patch("getgist.request.requests.get")
    requests = GetGistRequests({"foo": "bar"})
    requests.get("foobar", params=42, bar="foo")
    get.assert_called_once_with("foobar",
                                params=42,
                                headers={
                                    "foo": "bar",
                                    "bar": "foo"
                                })
Beispiel #4
0
class GitHubTools(GetGistCommons):
    """Helpers to deal with GitHub API and manipulate gists"""

    version = get_distribution("getgist").version
    api_root_url = "https://api.github.com/"
    headers = {
        "Accept": "application/vnd.github.v3+json",
        "User-Agent": "GetGist v{}".format(version),
    }
    requests = GetGistRequests(headers)
    is_authenticated = False

    def __init__(self, user, file_path, assume_yes=False):
        """
        Save basic variables to all methods, instantiate GetGistrequests and
        calls the OAuth method.
        :param user: (str) GitHub username
        :param file_path: (str or None) file_path to be saved (locally), created or updated (remotelly)
        :param assume_yes: (bool) assume yes (or first option) for all prompts
        :return: (None)
        """
        self.user = user
        self.file_path = file_path
        self.filename = os.path.basename(file_path) if file_path else None
        self.assume_yes = assume_yes
        self.add_oauth_header()

    def add_oauth_header(self):
        """
        Validate token and add the proper header for further requests.
        :return: (None)
        """
        # abort if no token
        oauth_token = self._get_token()
        if not oauth_token:
            return

        # add oauth header & reach the api
        self.headers["Authorization"] = "token " + oauth_token
        url = self._api_url("user")
        raw_resp = self.requests.get(url)
        resp = raw_resp.json()

        # abort & remove header if token is invalid
        if resp.get("login", None) != self.user:
            self.oops("Invalid token for user " + self.user)
            self.headers.pop("Authorization")
            return

        self.is_authenticated = True
        self.yeah("User {} authenticated".format(self.user))

    def get_gists(self):
        """
        List generator containing gist relevant information
        such as id, description, filenames and raw URL (dict).
        """
        # fetch all gists
        if self.is_authenticated:
            url = self._api_url("gists")
        else:
            url = self._api_url("users", self.user, "gists")
        self.output("Fetching " + url)
        raw_resp = self.requests.get(url)

        # abort if user not found
        if raw_resp.status_code != 200:
            self.oops("User `{}` not found".format(self.user))
            return

        # abort if there are no gists
        resp = raw_resp.json()
        if not resp:
            self.oops("No gists found for user `{}`".format(self.user))
            return

        # parse response
        for gist in raw_resp.json():
            yield self._parse_gist(gist)

    @with_filename_only
    def select_gist(self, allow_none=False):
        """
        Given the requested filename, it selects the proper gist; if more than
        one gist is found with the given filename, user is asked to choose.
        :allow_none: (bool) for `getgist` it should raise error if no gist is
        found, but setting this argument to True avoid this error, which is
        useful when `putgist` is calling this method
        :return: (dict) selected gist
        """
        # pick up all matching gists
        matches = list()
        for gist in self.get_gists():
            for gist_file in gist.get("files"):
                if self.filename == gist_file.get("filename"):
                    matches.append(gist)

        # abort if no match is found
        if not matches:
            if allow_none:
                return None
            else:
                msg = "No file named `{}` found in {}'s gists"
                self.oops(msg.format(self.file_path, self.user))
                if not self.is_authenticated:
                    self.warn("To access private gists set the GETGIST_TOKEN")
                    self.warn("(see `getgist --help` for details)")
                return False

        # return if there's is only one match
        if len(matches) == 1 or self.assume_yes:
            return matches.pop(0)

        return self._ask_which_gist(matches)

    def list_gists(self):
        """ Prints all gists names, filenames and URLs"""
        gists = tuple(
            FileFromGist(
                gist_file.get("filename"),
                gist.get("description"),
                gist.get("url"),
            )
            for gist in self.get_gists()
            for gist_file in gist.get("files")
        )
        self.tabulate(*gists)

    @with_filename_only
    def read_gist_file(self, gist):
        """
        Returns the contents of file hosted inside a gist at GitHub.
        :param gist: (dict) gist parsed by GitHubTools._parse()
        :return: (bytes) content of a gist loaded from GitHub
        """
        url = False
        files = gist.get("files")
        for gist_file in files:
            if gist_file.get("filename") == self.filename:
                url = gist_file.get("raw_url")
                break
        if url:
            self.output("Reading {}".format(url))
            response = self.requests.get(url)
            return response.content

    @with_filename_only
    @oauth_only
    def update(self, gist, content):
        """
        Updates the contents of file hosted inside a gist at GitHub.
        :param gist: (dict) gist parsed by GitHubTools._parse_gist()
        :param content: (str or bytes) to be written
        :return: (bool) indicatind the success or failure of the update
        """
        # abort if content is False
        if content is False:
            return False

        # request
        url = self._api_url("gists", gist.get("id"))
        data = {"files": {self.filename: {"content": content}}}
        self.output("Sending contents of {} to {}".format(self.file_path, url))
        response = self.requests.patch(url, data=dumps(data))

        # error
        if response.status_code != 200:
            self.oops("Could not update " + gist.get("description"))
            self.oops("PATCH request returned " + str(response.status_code))
            return False

        # success
        self.yeah("Done!")
        self.hey("The URL to this Gist is: {}".format(gist["url"]))
        return True

    @with_filename_only
    @oauth_only
    def create(self, content, **kwargs):
        """
        Create a new gist.
        :param gist: (dict) gist parsed by GitHubTools._parse()
        :param content: (str or bytes) to be written
        :param public: (bool) defines if the gist is public or private
        :return: (bool) indicating the success or failure of the creation
        """
        # abort if content is False
        if content is False:
            return False

        # set new gist
        public = bool(kwargs.get("public", True))
        data = {
            "description": self.filename,
            "public": public,
            "files": {self.filename: {"content": content}},
        }

        # send request
        url = self._api_url("gists")
        self.output("Sending contents of {} to {}".format(self.file_path, url))
        response = self.requests.post(url, data=dumps(data))

        # error
        if response.status_code != 201:
            self.oops("Could not create " + self.filename)
            self.oops("POST request returned " + str(response.status_code))
            return False

        # parse created gist
        gist = self._parse_gist(response.json())

        # success
        self.yeah("Done!")
        self.hey("The URL to this Gist is: {}".format(gist["url"]))
        return True

    @with_filename_only
    def _ask_which_gist(self, matches):
        """
        Asks user which gist to use in case of more than one gist matching the
        instance filename.
        :param matches: (list) of dictionaries generated within select_gists()
        :return: (dict) of the selected gist
        """
        # ask user which gist to use
        self.hey("Use {} from which gist?".format(self.filename))
        for count, gist in enumerate(matches, 1):
            self.hey("[{}] {}".format(count, gist.get("description")))

        # get the gist index
        selected = False
        while not selected:
            gist_index = prompt("Type the number: ", type=int) - 1
            try:
                selected = matches[gist_index]
            except IndexError:
                self.oops("Invalid number, please try again.")

        self.output("Using `{}` Gist".format(selected["description"]))
        return selected

    def _api_url(self, *args):
        """Get entrypoints adding arguments separated by slashes"""
        return self.api_root_url + "/".join(args)

    @staticmethod
    def _parse_gist(gist):
        """Receive a gist (dict) and parse it to GetGist"""

        # parse files
        files = list()
        file_names = sorted(filename for filename in gist["files"].keys())
        for name in file_names:
            files.append(
                dict(filename=name, raw_url=gist["files"][name].get("raw_url"))
            )

        # parse description
        description = gist["description"]
        if not description:
            names = sorted(f.get("filename") for f in files)
            description = names.pop(0)

        return dict(
            description=description,
            id=gist.get("id"),
            files=files,
            url=gist.get("html_url"),
        )

    @staticmethod
    def _get_token():
        """Retrieve username from env var"""
        return os.getenv("GETGIST_TOKEN")
Beispiel #5
0
class GitHubTools(GetGistCommons):
    """Helpers to deal with GitHub API and manipulate gists"""

    version = get_distribution('getgist').version
    api_root_url = 'https://api.github.com/'
    headers = {'Accept': 'application/vnd.github.v3+json',
               'User-Agent': 'GetGist v{}'.format(version)}
    requests = GetGistRequests(headers)
    is_authenticated = False

    def __init__(self, user, file_path, assume_yes=False):
        """
        Save basic variables to all methods, instantiate GetGistrequests and
        calls the OAuth method.
        :param user: (str) GitHub username
        :param file_path: (str) file_path to be saved (locally), created or
        updated (remotelly)
        :param assume_yes: (bool) assume yes (or first option) for all prompts
        :return: (None)
        """
        self.user = user
        self.file_path = file_path
        self.filename = os.path.basename(file_path)
        self.assume_yes = assume_yes
        self.add_oauth_header()

    def add_oauth_header(self):
        """
        Validate token and add the proper header for further requests.
        :return: (None)
        """
        # abort if no token
        oauth_token = self._get_token()
        if not oauth_token:
            return

        # add oauth header & reach the api
        self.headers['Authorization'] = 'token ' + oauth_token
        url = self._api_url('user')
        raw_resp = self.requests.get(url)
        resp = raw_resp.json()

        # abort & remove header if token is invalid
        if resp.get('login', None) != self.user:
            self.oops('Invalid token for user ' + self.user)
            self.headers.pop('Authorization')
            return

        self.is_authenticated = True
        self.yeah('User {} authenticated'.format(self.user))

    def get_gists(self):
        """
        List generator containing gist relevant information
        such as id, description, filenames and raw URL (dict).
        """
        # fetch all gists
        if self.is_authenticated:
            url = self._api_url('gists')
        else:
            url = self._api_url('users', self.user, 'gists')
        self.output('Fetching ' + url)
        raw_resp = self.requests.get(url)

        # abort if user not found
        if raw_resp.status_code != 200:
            self.oops('User `{}` not found'.format(self.user))
            return

        # abort if there are no gists
        resp = raw_resp.json()
        if not resp:
            self.oops('No gists found for user `{}`'.format(self.user))
            return

        # parse response
        for gist in raw_resp.json():
            yield self._parse_gist(gist)

    def select_gist(self, allow_none=False):
        """
        Given the requested filename, it selects the proper gist; if more than
        one gist is found with the given filename, user is asked to choose.
        :allow_none: (bool) for `getgist` it should raise error if no gist is
        found, but setting this argument to True avoid this error, which is
        useful when `putgist` is calling this method
        :return: (dict) selected gist
        """
        # pick up all macthing gists
        matches = list()
        for gist in self.get_gists():
            for gist_file in gist.get('files'):
                if self.filename == gist_file.get('filename'):
                    matches.append(gist)

        # abort if no match is found
        if not matches:
            if allow_none:
                return None
            else:
                msg = "No file named `{}` found in {}'s gists"
                self.oops(msg.format(self.file_path, self.user))
                if not self.is_authenticated:
                    self.warn('To access private gists set the GETGIST_TOKEN')
                    self.warn('(see `getgist --help` for details)')
                return False

        # return if there's is only one match
        if len(matches) == 1 or self.assume_yes:
            return matches.pop(0)

        return self._ask_which_gist(matches)

    def read_gist_file(self, gist):
        """
        Returns the contents of file hosted inside a gist at GitHub.
        :param gist: (dict) gist parsed by GitHubTools._parse()
        :return: (bytes) content of a gist loaded from GitHub
        """
        url = False
        files = gist.get('files')
        for gist_file in files:
            if gist_file.get('filename') == self.filename:
                url = gist_file.get('raw_url')
                break
        if url:
            self.output('Reading {}'.format(url))
            response = self.requests.get(url)
            return response.content

    @oauth_only
    def update(self, gist, content):
        """
        Updates the contents of file hosted inside a gist at GitHub.
        :param gist: (dict) gist parsed by GitHubTools._parse_gist()
        :param content: (str or bytes) to be written
        :return: (bool) indicatind the success or failure of the update
        """
        # abort if content is False
        if content is False:
            return False

        # request
        url = self._api_url('gists', gist.get('id'))
        data = {'files': {self.filename: {'content': content}}}
        self.output('Sending contents of {} to {}'.format(self.file_path, url))
        response = self.requests.patch(url, data=dumps(data))

        # error
        if response.status_code != 200:
            self.oops('Could not update ' + gist.get('description'))
            self.oops('PATCH request returned ' + str(response.status_code))
            return False

        # success
        self.yeah('Done!')
        self.hey('The URL to this Gist is: {}'.format(gist['url']))
        return True

    @oauth_only
    def create(self, content, **kwargs):
        """
        Create a new gist.
        :param gist: (dict) gist parsed by GitHubTools._parse()
        :param content: (str or bytes) to be written
        :param public: (bool) defines if the gist is public or private
        :return: (bool) indicatind the success or failure of the creation
        """
        # abort if content is False
        if content is False:
            return False

        # set new gist
        public = bool(kwargs.get('public', True))
        data = {'description': self.filename, 'public': public,
                'files': {self.filename: {'content': content}}}

        # send request
        url = self._api_url('gists')
        self.output('Sending contents of {} to {}'.format(self.file_path, url))
        response = self.requests.post(url, data=dumps(data))

        # error
        if response.status_code != 201:
            self.oops('Could not create ' + self.filename)
            self.oops('POST request returned ' + str(response.status_code))
            return False

        # parse created gist
        gist = self._parse_gist(response.json())

        # success
        self.yeah('Done!')
        self.hey('The URL to this Gist is: {}'.format(gist['url']))
        return True

    def _ask_which_gist(self, matches):
        """
        Asks user which gist to use in case of more than one gist matching the
        instance filename.
        :param matches: (list) of dictioaries generated within select_gists()
        :return: (dict) of the selected gist
        """
        # ask user which gist to use
        self.hey('Use {} from which gist?'.format(self.filename))
        for count, gist in enumerate(matches, 1):
            self.hey('[{}] {}'.format(count, gist.get('description')))

        # get the gist index
        selected = False
        while not selected:
            try:
                gist_index = int(self.ask('Type the number: ')) - 1
                selected = matches[gist_index]
            except (ValueError, IndexError):
                self.oops('Invalid number, please try again.')

        self.output('Using `{}` Gist'.format(selected['description']))
        return selected

    def _api_url(self, *args):
        """Get entrypoints adding arguments separated by slashes"""
        return self.api_root_url + '/'.join(args)

    @staticmethod
    def _parse_gist(gist):
        """Receive a gist (dict) and parse it to GetGist"""

        # parse files
        files = list()
        file_names = sorted(filename for filename in gist['files'].keys())
        for name in file_names:
            files.append(dict(filename=name,
                              raw_url=gist['files'][name].get('raw_url')))

        # parse description
        description = gist['description']
        if not description:
            names = sorted(f.get('filename') for f in files)
            description = names.pop(0)

        return dict(description=description,
                    id=gist.get('id'),
                    files=files,
                    url=gist.get('html_url'))

    @staticmethod
    def _get_token():
        """Retrieve username from env var"""
        return os.getenv('GETGIST_TOKEN')
Beispiel #6
0
def test_update_header():
    requests = GetGistRequests({"foo": "bar"})
    assert requests.headers["foo"] == "bar"
Beispiel #7
0
def test_no_header():
    requests = GetGistRequests()
    assert isinstance(requests.headers, dict)
Beispiel #8
0
def test_add_header():
    requests = GetGistRequests({"foo": "bar"})
    new_headers = requests.add_headers({"headers": {"bar": "foo"}})
    assert new_headers == {"headers": {"foo": "bar", "bar": "foo"}}
Beispiel #9
0
 def test_no_header(self):
     requests = GetGistRequests()
     self.assertIsInstance(requests.headers, dict)
Beispiel #10
0
 def test_add_header(self):
     requests = GetGistRequests({'foo': 'bar'})
     got = requests.add_headers({'headers': {'bar': 'foo'}})
     self.assertEqual({'headers': {'foo': 'bar', 'bar': 'foo'}}, got)
Beispiel #11
0
 def test_update_header(self):
     requests = GetGistRequests({'foo': 'bar'})
     self.assertEqual(requests.headers['foo'], 'bar')