diff --git a/github3/repos/repo.py b/github3/repos/repo.py index 68d972641..68479d83f 100644 --- a/github3/repos/repo.py +++ b/github3/repos/repo.py @@ -40,6 +40,7 @@ from . import stats from . import status from . import tag +from . import topics from ..decorators import requires_auth @@ -51,6 +52,10 @@ class _Repository(models.GitHubCore): method to ensure that all attributes are present on the object. """ + PREVIEW_HEADERS = { + 'Accept': 'application/vnd.github.mercy-preview+json' + } + STAR_HEADERS = { 'Accept': 'application/vnd.github.v3.star+json' } @@ -2161,6 +2166,31 @@ def remove_collaborator(self, username): base_url=self._api) return self._boolean(self._delete(url), 204, 404) + @requires_auth + def replace_topics(self, new_topics): + """Replace the repository topics with ``new_topics``. + + :param topics: + (required), new topics of the repository + :type topics: + list + :returns: + new topics of the repository + :rtype: + :class:`~github3.repos.topics.Topics` + """ + url = self._build_url('topics', base_url=self._api) + data = {'names': new_topics} + json = self._json( + self._put( + url, + data=jsonlib.dumps(data), + headers=self.PREVIEW_HEADERS + ), + 200 + ) + return self._instance_or_null(topics.Topics, json) + def stargazers(self, number=-1, etag=None): """List users who have starred this repository. @@ -2310,6 +2340,18 @@ def teams(self, number=-1, etag=None): url = self._build_url('teams', base_url=self._api) return self._iter(int(number), url, orgs.ShortTeam, etag=etag) + def topics(self): + """Get the topics of this repository. + + :returns: + this repository's topics + :rtype: + :class:`~github3.repos.topics.Topics` + """ + url = self._build_url('topics', base_url=self._api) + json = self._json(self._get(url, headers=self.PREVIEW_HEADERS), 200) + return self._instance_or_null(topics.Topics, json) + def tree(self, sha): """Get a tree. diff --git a/github3/repos/topics.py b/github3/repos/topics.py new file mode 100644 index 000000000..06057eef2 --- /dev/null +++ b/github3/repos/topics.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +"""Topics related logic.""" +from __future__ import unicode_literals + +from .. import models + + +class Topics(models.GitHubCore): + """Representation of the repository topics. + + .. attribute:: names + + The names of the topics. + """ + + def _update_attributes(self, topics): + self.names = topics['names'] + + def _repr(self): + return ''.format(', '.join(self.names)) diff --git a/tests/cassettes/Repository_replace_topics.json b/tests/cassettes/Repository_replace_topics.json new file mode 100644 index 000000000..19681c2d0 --- /dev/null +++ b/tests/cassettes/Repository_replace_topics.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"], "Authorization": ["token "]}, "method": "GET", "uri": "https://api.github.com/repos/jacquerie/flask-shell-bpython"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA62Yb3PiNhDGvwrD2wYM5D8zmWs7vblJp1zbG669yRtG2AIryJJPkqHEk+/eR5KNbSaXOInfJGC0Pz1e7653lfdZ1J9ejM8vJ9eTy5O+kBFd2Ev92W8fd3/y33n46fqBfPuyDcVmMpt/PZ3db/afdzc3fSwmCcXKFSd6M9Ax5XywTPcmlgI/rjLOF8WKexJ+z6hiNHh6rdwJqvrTvM/lmgkgDwYAWTWnV+PJ1agp7++Lf7595uH97Gw2vz2b/eIkkS0xRC0yxUGJjUn1NAj8RT0ZrpmJs2WmqQqlMFSYYSiTIAs8/sP25gyItSogzg24cARLWcHxxoDpoK43Ngk/EuD3devrK1eSc7mD/bHeZ7cIDmbWyw7BxPotCJjlgTQxhcNwG4/25pk2r5TjTPLA/kPkWIjGI1A0ep2kwgiCbDA85oGiqXS0bKlDxVLDpHiltIYpUFKtiWAP5A0omGoQrKhXinAmMKVbBNwrbb1NHqSKbUm4t+5QNKRsC+++hXdkDJzZpzaHv+L5W18zQxckSmwSrgjX9PGk7/Y2WOQunCCrWsX3D1I9ooeHiW2/0JSTkPYQg72IrkjGTc+ViJ4rJz1kTEJE1NshcXukp1nCOFE9KWhPZUIggHu//uUqzhDqV1JtDjKfzVr3TKpc/IFWy3vhgbUAIV+BgbwN3XdAs5Q8wN8i2UJUALKUihj5UiFpI7aBy4P6Vxt9hpKkg5twGOBiKbvwsMMAx7TOaKvEaOMKR9NBmYMiS5a+SLbJvDYbeA50E63ZWlDagWcPqDwoa/pSERHGXcBLUh74Ty4iyLoD2ZYC2JLLZQc0vG8Dh8oDHRP/XjOLbpRatiU10IquOpJtSQe0UZ3EhJNsUQcwXrYG4dGB5pIU5IWnORHrjKy7YB9QiAzbHqzJw4vNUpu8q1gA205QsWXWVfmsaFa1711QN7pwdQWr0K4ter7TauWSWpPlnJIk7KVmpQ23ADWSpTO4jevjDez3l/usttItKQ+qyu9fMMUe7/d68YYpNdd3KgaUDsKmJAX5Tykxsa2G2DAlir7/BgpQkC8JesXhcJjHlLgZIKGqkxrgOQASFcboe9+vOS9J6MsSYtyksbKSI0weXJKoA58fUMD6h/x+3Z5Tj5EUI3YHYh2mzkWTTbVBk90BvGLVdxDSsBUL2wxibVK1gcs/aCZCekI4P0GUGxYyxD1GBfuM0SrTLnzmObglHIH4qYxTpEAHDlPUk/LAD9MRRiS576im1WC2DCiKwS5aEINRbDIaXwxGF4PJaD6+no4m0/HZHdZkadRYczUYXQ7Gp3MsOL2YjtyaNMO0VmGw5HwwwpKr6eR0OrqyS1CqiwzAJxzJPHUi8tQUZo9aYK51XJn/XBlPnz1ZKoxDjlA+yr/X7r89fse2BUB8LBOaoiPyp0maPeDTeNxoaUKZCTyE05P+jhj06mgXqktlGwR7P+taJtELXxD6U6MyO5bjSqrkPQ2Nrl+rSlFt4Y5tWMPQNmyH0dnPvZWAhCkli0M1gWJxKMY4ICsOBWRKRSGoVD5BtrGQCo3bze3wC/1oKiC+OBWc3c57fxQr4I00+q84ebyd28BrHuE1j8QKsA4KYO3UMryYf+L3d/+eP9zNPz70cYDh52/r3JrK/hT6Smc7zxfnDws/2lixRBt3KpJSlcDZ9vzJ3kpxPuLdbmO/9KQtpP4zdkX5kbuF/p4RxK57i5XL/C/uEnxlG63mL4rat2nTRlCzwzFCLSrqrWXxqMaP/wPrRXVIzxUAAA==", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Last-Modified": ["Fri, 13 Jul 2018 02:36:04 GMT"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["9A4E:7DB6:8AB0F67:11AD54DE:5B48110E"], "ETag": ["W/\"7107ffa6cfdb88d58106f3cb7649cbfb\""], "Date": ["Fri, 13 Jul 2018 02:40:15 GMT"], "X-RateLimit-Remaining": ["4992"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-OAuth-Scopes": ["public_repo"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.057600"], "Vary": ["Accept, Authorization, Cookie, X-GitHub-OTP"], "X-RateLimit-Limit": ["5000"], "Cache-Control": ["private, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Accepted-OAuth-Scopes": ["repo"], "X-RateLimit-Reset": ["1531452513"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/jacquerie/flask-shell-bpython"}, "recorded_at": "2018-07-13T02:40:15"}, {"request": {"body": {"string": "{\"names\": [\"flask\", \"bpython\", \"python\"]}", "encoding": "utf-8"}, "headers": {"Content-Length": ["41"], "Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.mercy-preview+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"], "Authorization": ["token "]}, "method": "PUT", "uri": "https://api.github.com/repos/jacquerie/flask-shell-bpython/topics"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA6tWykvMTS1WsopWSstJLM5W0lFKKqgsycjPA7KgjNhaAGF4mvYmAAAA", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["9A4E:7DB6:8AB0F76:11AD54FF:5B48110F"], "ETag": ["W/\"8ae34e77215a659579641afb01a6a267\""], "Date": ["Fri, 13 Jul 2018 02:40:15 GMT"], "X-RateLimit-Remaining": ["4991"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-OAuth-Scopes": ["public_repo"], "X-GitHub-Media-Type": ["github.mercy-preview; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.105731"], "Vary": ["Accept, Authorization, Cookie, X-GitHub-OTP"], "X-RateLimit-Limit": ["5000"], "Cache-Control": ["private, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-Accepted-OAuth-Scopes": ["repo"], "X-RateLimit-Reset": ["1531452513"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/jacquerie/flask-shell-bpython/topics"}, "recorded_at": "2018-07-13T02:40:15"}], "recorded_with": "betamax/0.8.1"} \ No newline at end of file diff --git a/tests/cassettes/Repository_topics.json b/tests/cassettes/Repository_topics.json new file mode 100644 index 000000000..8d86430c1 --- /dev/null +++ b/tests/cassettes/Repository_topics.json @@ -0,0 +1 @@ +{"http_interactions": [{"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.v3.full+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/repos/jacquerie/flask-shell-bpython"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA62Yb2/bNhDGv4qht7NN2/lvIOg2rCgyzO1WuFuRNwYt0RYTitRIyp4t5LvvISVbspEmcqI3TaLyfnx0vDvdMQ94FIwvhxdXo5vRVTeQKmIz9yiY/PZx/UX8LsJPN1v6/esqlI+jyfTb2eThcfN5fXsbYDFNGFYuBDWPPRMzIXrzdGNjJfGfi0yIWbnigYb/ZkxzRp5fq9aS6WCcB0ItuQRybwCQU3N2PRxdDw7l/XX59/fPInyYnE+md+eTX7wkuqKW6lmmBSixtakZE1I8NKP+kts4m2eG6VBJy6TthyohGSnwH1a350AsdQnxbsCDI1jKS05hDJghdb2xTcSRgGJfv76+cqGEUGvYH+t9cQuyN3Ne9ggul29BwCwnysYMDsNrPLmX58aeKMeb5MT9QOQ4iMERaBadJqk0giAXDE850SxVnpbNTah5armSJ0o7MAVK6SWVfEvfgIKpAcGJOlGEN4EpWyHgTrQtbHKSar6i4ca5Q7OQ8RW8+xbekTFwdpO6HP6G83e+5pbNaJS4JFxQYdhTN/B7WyzyD7rIqkbx/YNUj9j+MLHtV5YKGrIOYrATsQXNhO34EtHx5aSDjEmojDprJG6HdgxPuKC6oyTr6ExKBHDn1z99xelD/ULpx73MF7PWn0mViz/Q6nivHFgDEPIVGMh7ZJsWaI6SE/xbJluICkDnSlOrXiskTcQe4HJS/9NFn2U0aeElPAa4WKk2POwxwHFjMtYoMZq4wtMM2eWgzJJ5USSbZF6TDQoOdFNj+FIy1oJn96ic7Gr6XFMZxm3Ad6ScFL/5iKDLFmQ7CmBzoeYt0PC9JR6VExPT4rtmZ+0odWxHOkBrtmhJtiPt0Va3EhNeskPtwfjYWoRHC5p3JJKXnhZULjO6bIO9RyEyXHuwpNtXm6UmeVexAHadoObzrK3yWdGc6qJ3Qd1ow9UVrEL7tujlTquRS2pNlndKkvDXmpUm3BJ0kCytwV1cH2/g/n69z2oq3ZFyUlX+4gNT7vF+r5dfmJ3m+k7lgNJC2OxIJP8ppTZ21RAbplSz979ACSL5nKJX7Pf7ecyonwESplupAQUHQKrDGH3v+zXnOxL6soRaP2ksnOQIk4dQNGrB53sUsMUhv193wanHSIoRuwWxHlPnoslmxqLJbgFeseo7SGX5godNBrEmqXqAyz8YLkPWpUJ0EeWWhxxxj1HBnTFaZdaGzwoOXglXIMVUJhhSoAWHaVaQclIM0xFGJLVpqabVYK4MaIbBLppRi1FsNBhe9gaXvdFgOrwZD0bj4fk91mRpdLDmuje46g3PplhwPhgPL9yaNMO0VmGw5KI3wJLr8ehsPLh2S1CqywzAb7iSee5G5LkpzF21wNyYuDL/uTIev3izVBqHAqF8lH+n7r86/sY2BUB8rBKWoiMqbpMM3+K34fCgpQlVJnEIZ91gTS16dbQL1aNdGwT7YtZ1TGpmRUEIxlZnbizHk1SrBxZaU39WlaLawjV/5AeGrmHbj87F3FsJSLjWqrxUkygW+2KMC7LyUkClTJaCdspHyDYeMmnwurkbfqEfTQXEl7eCk7tp549yBbyRRv+VN493Uxd4h1d4h1diJdiQEli7tQwvp5/Ew/0/F9v76cdtgAuMYv52zq2pDMbQt3O293x5/zArRhsnlhrrb0Uks2vM8LUjqfd1pZ+GT/8D5BPhF0wVAAA=", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Last-Modified": ["Fri, 13 Jul 2018 02:40:15 GMT"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["9AC2:7DB7:B5F3A84:1538CBC2:5B481450"], "ETag": ["W/\"d029658658da401bc28144b5257f64c6\""], "Date": ["Fri, 13 Jul 2018 02:54:09 GMT"], "X-RateLimit-Remaining": ["57"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.046247"], "Vary": ["Accept"], "X-RateLimit-Limit": ["60"], "Cache-Control": ["public, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-RateLimit-Reset": ["1531452470"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/jacquerie/flask-shell-bpython"}, "recorded_at": "2018-07-13T02:54:09"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Accept-Encoding": ["gzip, deflate"], "Accept": ["application/vnd.github.mercy-preview+json"], "User-Agent": ["github3.py/1.1.0"], "Accept-Charset": ["utf-8"], "Connection": ["keep-alive"], "Content-Type": ["application/json"]}, "method": "GET", "uri": "https://api.github.com/repos/jacquerie/flask-shell-bpython/topics"}, "response": {"body": {"string": "", "base64_string": "H4sIAAAAAAAAA6tWykvMTS1WsopWSstJLM5W0lFKKqgsycjPA7KgjNhaAGF4mvYmAAAA", "encoding": "utf-8"}, "headers": {"X-XSS-Protection": ["1; mode=block"], "Content-Security-Policy": ["default-src 'none'"], "Access-Control-Expose-Headers": ["ETag, Link, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval"], "Transfer-Encoding": ["chunked"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["200 OK"], "X-GitHub-Request-Id": ["9AC2:7DB7:B5F3AD4:1538CC0C:5B481451"], "ETag": ["W/\"61a23bad4105517f68226fb33bebcb2d\""], "Date": ["Fri, 13 Jul 2018 02:54:09 GMT"], "X-RateLimit-Remaining": ["56"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-GitHub-Media-Type": ["github.mercy-preview; format=json"], "X-Content-Type-Options": ["nosniff"], "Content-Encoding": ["gzip"], "X-Runtime-rack": ["0.048572"], "Vary": ["Accept"], "X-RateLimit-Limit": ["60"], "Cache-Control": ["public, max-age=60, s-maxage=60"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/json; charset=utf-8"], "X-RateLimit-Reset": ["1531452470"]}, "status": {"message": "OK", "code": 200}, "url": "https://api.github.com/repos/jacquerie/flask-shell-bpython/topics"}, "recorded_at": "2018-07-13T02:54:09"}], "recorded_with": "betamax/0.8.1"} \ No newline at end of file diff --git a/tests/integration/test_repos_repo.py b/tests/integration/test_repos_repo.py index 41053f334..dce6785ba 100644 --- a/tests/integration/test_repos_repo.py +++ b/tests/integration/test_repos_repo.py @@ -1155,6 +1155,17 @@ def test_remove_collaborator(self): assert removed_collaborator is True + def test_replace_topics(self): + """Test the ability to replace the topics of a repository.""" + self.token_login() + cassette_name = self.cassette_name('replace_topics') + with self.recorder.use_cassette(cassette_name): + repository = self.gh.repository('jacquerie', 'flask-shell-bpython') + topics = repository.replace_topics(['flask', 'bpython', 'python']) + + assert isinstance(topics, github3.repos.topics.Topics) + assert len(topics.names) == 3 + def test_stargazers(self): """Test the ability to retrieve the stargazers on a repository.""" cassette_name = self.cassette_name('stargazers') @@ -1251,6 +1262,16 @@ def test_teams(self): for team in teams: assert isinstance(team, github3.orgs.ShortTeam) + def test_topics(self): + """Test the ability to retrieve topics from a repository.""" + cassette_name = self.cassette_name('topics') + with self.recorder.use_cassette(cassette_name): + repository = self.gh.repository('jacquerie', 'flask-shell-bpython') + topics = repository.topics() + + assert isinstance(topics, github3.repos.topics.Topics) + assert len(topics.names) == 3 + def test_tree(self): """Test the ability to retrieve a tree from a repository.""" cassette_name = self.cassette_name('tree') diff --git a/tests/unit/test_repos_repo.py b/tests/unit/test_repos_repo.py index a43ffc6da..1152bfc1a 100644 --- a/tests/unit/test_repos_repo.py +++ b/tests/unit/test_repos_repo.py @@ -972,6 +972,16 @@ def test_remove_collaborator_required_username(self): assert self.session.delete.called is False + def test_replace_topics(self): + """Verify the request for replacing the topics.""" + self.instance.replace_topics(['flask', 'bpython', 'python']) + + self.session.put.assert_called_once_with( + url_for('topics'), + data='{"names": ["flask", "bpython", "python"]}', + headers=self.instance.PREVIEW_HEADERS + ) + def test_source(self): """Verify that the source of the repository can be retrieved.""" source = self.instance.source @@ -1000,6 +1010,15 @@ def test_tag_required_sha(self): assert self.session.get.called is False + def test_topics(self): + """Verify the request for retrieving the topics.""" + self.instance.topics() + + self.session.get.assert_called_once_with( + url_for('topics'), + headers=self.instance.PREVIEW_HEADERS + ) + def test_tree(self): """Verify the request for retrieving a tree.""" self.instance.tree('fake-sha') @@ -1664,6 +1683,10 @@ def test_remove_collaborator(self): """Show that a user must be authenticated to remove a collaborator.""" self.assert_requires_auth(self.instance.remove_collaborator) + def test_replace_topics(self): + """Show that a user must be authenticated to replace the topics.""" + self.assert_requires_auth(self.instance.replace_topics) + def test_subscription(self): """ Show that a user must be authenticated to retrieve the