Skip to content

Commit 5567fb1

Browse files
committed
Finish adding the Contents CrUD API
1 parent e522baf commit 5567fb1

File tree

4 files changed

+247
-22
lines changed

4 files changed

+247
-22
lines changed

HISTORY.rst

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ History/Changelog
99
- Add ``check_authorization`` to the ``GitHub class`` to cover the `new part
1010
of the API <http://developer.github.com/v3/oauth/#check-an-authorization>`_.
1111

12-
- Add ``create_file``, ``iter_contributor_statistics``,
13-
``iter_commit_activity``, ``iter_code_frequency`` and
14-
``weekly_commit_count`` to the ``Repository`` object.
12+
- Add ``create_file``, ``update_file``, ``delete_file``,
13+
``iter_contributor_statistics``, ``iter_commit_activity``,
14+
``iter_code_frequency`` and ``weekly_commit_count`` to the ``Repository``
15+
object.
16+
17+
- Add ``update`` and ``delete`` methods to the ``Contents`` object.
1518

1619
- Add ``is_following`` to the ``User`` object.
1720

github3/repos/contents.py

Lines changed: 71 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,14 @@
77
88
"""
99

10-
from base64 import b64decode
11-
from github3.models import GitHubObject
10+
from json import dumps
11+
from base64 import b64decode, b64encode
12+
from github3.git import Commit
13+
from github3.models import GitHubCore
14+
from github3.decorators import requires_auth
1215

1316

14-
class Contents(GitHubObject):
17+
class Contents(GitHubCore):
1518
"""The :class:`Contents <Contents>` object. It holds the information
1619
concerning any content in a repository requested via the API.
1720
@@ -27,8 +30,8 @@ class Contents(GitHubObject):
2730
2831
See also: http://developer.github.com/v3/repos/contents/
2932
"""
30-
def __init__(self, content):
31-
super(Contents, self).__init__(content)
33+
def __init__(self, content, session=None):
34+
super(Contents, self).__init__(content, session)
3235
# links
3336
self._api = content.get('url')
3437
#: Dictionary of links
@@ -88,3 +91,66 @@ def __eq__(self, other):
8891

8992
def __ne__(self, other):
9093
return self.sha != other
94+
95+
@requires_auth
96+
def delete(self, message, committer=None, author=None):
97+
"""Delete this file.
98+
99+
:param str message: (required), commit message to describe the removal
100+
:param dict committer: (optional), if no information is given the
101+
authenticated user's information will be used. You must specify
102+
both a name and email.
103+
:param dict author: (optional), if omitted this will be filled in with
104+
committer information. If passed, you must specify both a name and
105+
email.
106+
:returns: :class:`Commit <github3.git.Commit>`
107+
108+
"""
109+
json = None
110+
if message:
111+
data = {'message': message, 'sha': self.sha,
112+
'committer': validate_commmitter(committer),
113+
'author': validate_commmitter(author)}
114+
self._remove_none(data)
115+
json = self._json(self._delete(self._api, data=dumps(data)), 200)
116+
if 'commit' in json:
117+
json = Commit(json['commit'], self)
118+
return json
119+
120+
@requires_auth
121+
def update(self, message, content, committer=None, author=None):
122+
"""Update this file.
123+
124+
:param str message: (required), commit message to describe the update
125+
:param str content: (required), content to update the file with
126+
:param dict committer: (optional), if no information is given the
127+
authenticated user's information will be used. You must specify
128+
both a name and email.
129+
:param dict author: (optional), if omitted this will be filled in with
130+
committer information. If passed, you must specify both a name and
131+
email.
132+
:returns: :class:`Commit <github3.git.Commit>`
133+
134+
"""
135+
if content and not isinstance(content, bytes):
136+
raise ValueError( # (No coverage)
137+
'content must be a bytes object') # (No coverage)
138+
139+
json = None
140+
if message and content:
141+
content = b64encode(content).decode('utf-8')
142+
data = {'message': message, 'content': content, 'sha': self.sha,
143+
'committer': validate_commmitter(committer),
144+
'author': validate_commmitter(author)}
145+
self._remove_none(data)
146+
json = self._json(self._put(self._api, data=dumps(data)), 200)
147+
if 'content' in json and 'commit' in json:
148+
self.__init__(json['content'], self)
149+
json = Commit(json['commit'], self)
150+
return json
151+
152+
153+
def validate_commmitter(d):
154+
if d and d.get('name') and d.get('email'):
155+
return d
156+
return None

github3/repos/repo.py

Lines changed: 85 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
from github3.repos.comment import RepoComment
2525
from github3.repos.commit import RepoCommit
2626
from github3.repos.comparison import Comparison
27-
from github3.repos.contents import Contents
27+
from github3.repos.contents import Contents, validate_commmitter
2828
from github3.repos.download import Download
2929
from github3.repos.hook import Hook
3030
from github3.repos.status import Status
@@ -304,9 +304,9 @@ def contents(self, path, ref=None):
304304
url = self._build_url('contents', path, base_url=self._api)
305305
json = self._json(self._get(url, params={'ref': ref}), 200)
306306
if isinstance(json, dict):
307-
return Contents(json)
307+
return Contents(json, self)
308308
elif isinstance(json, list):
309-
return dict((j.get('name'), Contents(j)) for j in json)
309+
return dict((j.get('name'), Contents(j, self)) for j in json)
310310
return None
311311

312312
@requires_auth
@@ -409,18 +409,14 @@ def create_file(self, path, message, content, branch=None,
409409
if path and message and content:
410410
url = self._build_url('contents', path, base_url=self._api)
411411
content = b64encode(content).decode('utf-8')
412-
413-
data = {'message': message, 'content': content,
414-
'branch': branch}
415-
if committer and committer.get('name') and committer.get('email'):
416-
data.update(committer=committer)
417-
if author and author.get('name') and author.get('email'):
418-
data.update(author=author)
419-
412+
data = {'message': message, 'content': content, 'branch': branch,
413+
'committer': validate_commmitter(committer),
414+
'author': validate_commmitter(author)}
420415
self._remove_none(data)
421416
json = self._json(self._put(url, data=dumps(data)), 201)
422-
json['content'] = Contents(json['content'])
423-
json['commit'] = Commit(json['commit'], self)
417+
if 'content' in json and 'commit' in json:
418+
json['content'] = Contents(json['content'], self)
419+
json['commit'] = Commit(json['commit'], self)
424420
return json
425421

426422
@requires_auth
@@ -674,6 +670,41 @@ def delete(self):
674670
"""
675671
return self._boolean(self._delete(self._api), 204, 404)
676672

673+
@requires_auth
674+
def delete_file(self, path, message, sha, branch=None, committer=None,
675+
author=None):
676+
"""Delete the file located at ``path``.
677+
678+
This is part of the Contents CrUD (Create Update Delete) API. See
679+
http://developer.github.com/v3/repos/contents/#delete-a-file for more
680+
information.
681+
682+
:param str path: (required), path to the file being removed
683+
:param str message: (required), commit message for the deletion
684+
:param str sha: (required), blob sha of the file being removed
685+
:param str branch: (optional), if not provided, uses the repository's
686+
default branch
687+
:param dict committer: (optional), if no information is given the
688+
authenticated user's information will be used. You must specify
689+
both a name and email.
690+
:param dict author: (optional), if omitted this will be filled in with
691+
committer information. If passed, you must specify both a name and
692+
email.
693+
:returns: :class:`Commit <github3.git.Commit>` if successful
694+
695+
"""
696+
json = None
697+
if path and message and sha:
698+
url = self._build_url('contents', path, base_url=self._api)
699+
data = {'message': message, 'sha': sha, 'branch': branch,
700+
'committer': validate_commmitter(committer),
701+
'author': validate_commmitter(author)}
702+
self._remove_none(data)
703+
json = self._json(self._delete(url, data=dumps(data)), 200)
704+
if json and 'commit' in json:
705+
json = Commit(json['commit'])
706+
return json
707+
677708
@requires_auth
678709
def delete_key(self, key_id):
679710
"""Delete the key with the specified id from your deploy keys list.
@@ -1382,7 +1413,7 @@ def readme(self):
13821413
"""
13831414
url = self._build_url('readme', base_url=self._api)
13841415
json = self._json(self._get(url), 200)
1385-
return Contents(json) if json else None
1416+
return Contents(json, self) if json else None
13861417

13871418
def ref(self, ref):
13881419
"""Get a reference pointed to by ``ref``.
@@ -1466,6 +1497,46 @@ def tree(self, sha):
14661497
json = self._json(self._get(url), 200)
14671498
return Tree(json, self) if json else None
14681499

1500+
@requires_auth
1501+
def update_file(self, path, message, content, sha, branch=None,
1502+
author=None, committer=None):
1503+
"""Update the file ``path`` with ``content``.
1504+
1505+
This is part of the Contents CrUD (Create Update Delete) API. See
1506+
http://developer.github.com/v3/repos/contents/#update-a-file for more
1507+
information.
1508+
1509+
:param str path: (required), path to the file being updated
1510+
:param str message: (required), commit message
1511+
:param str content: (required), updated contents of the file
1512+
:param str sha: (required), blob sha of the file being updated
1513+
:param str branch: (optional), uses the default branch on the
1514+
repository if not provided.
1515+
:param dict author: (optional), if omitted this will be filled in with
1516+
committer information. If passed, you must specify both a name and
1517+
email.
1518+
:returns: {'commit': :class:`Commit <github3.git.Commit>`,
1519+
'content': :class:`Contents <github3.repos.contents.Contents>`}
1520+
1521+
"""
1522+
if content and not isinstance(content, bytes):
1523+
raise ValueError( # (No coverage)
1524+
'content must be a bytes object') # (No coverage)
1525+
1526+
json = None
1527+
if path and message and content and sha:
1528+
url = self._build_url('contents', path, base_url=self._api)
1529+
content = b64encode(content).decode('utf-8')
1530+
data = {'message': message, 'content': content, 'sha': sha,
1531+
'committer': validate_commmitter(committer),
1532+
'author': validate_commmitter(author)}
1533+
self._remove_none(data)
1534+
json = self._json(self._put(url, data=dumps(data)), 200)
1535+
if 'content' in json and 'commit' in json:
1536+
json['content'] = Contents(json['content'], self)
1537+
json['commit'] = Commit(json['commit'], self)
1538+
return json
1539+
14691540
@requires_auth
14701541
def update_label(self, name, color, new_name=''):
14711542
"""Update the label ``name``.

tests/test_repos.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1046,6 +1046,44 @@ def test_create_file(self):
10461046
expect(ret['content']).isinstance(repos.contents.Contents)
10471047
self.mock_assertions()
10481048

1049+
def test_update_file(self):
1050+
self.response('create_content', 200)
1051+
self.put(self.api + 'contents/setup.py')
1052+
self.conf = {
1053+
'data': {
1054+
'message': 'foo',
1055+
'content': 'Zm9vIGJhciBib2d1cw==',
1056+
'sha': 'ae02db',
1057+
}
1058+
}
1059+
1060+
with expect.githuberror():
1061+
self.repo.update_file(None, None, None, None)
1062+
1063+
self.not_called()
1064+
self.login()
1065+
1066+
ret = self.repo.update_file('setup.py', 'foo', b'foo bar bogus',
1067+
'ae02db')
1068+
expect(ret).isinstance(dict)
1069+
expect(ret['commit']).isinstance(github3.git.Commit)
1070+
expect(ret['content']).isinstance(repos.contents.Contents)
1071+
self.mock_assertions()
1072+
1073+
def test_delete_file(self):
1074+
self.response('create_content', 200)
1075+
self.delete(self.api + 'contents/setup.py')
1076+
self.conf = {'data': {'message': 'foo', 'sha': 'ae02db'}}
1077+
1078+
with expect.githuberror():
1079+
self.repo.delete_file('setup.py', None, None)
1080+
1081+
self.not_called()
1082+
self.login()
1083+
ret = self.repo.delete_file('setup.py', 'foo', 'ae02db')
1084+
expect(ret).isinstance(github3.git.Commit)
1085+
self.mock_assertions()
1086+
10491087
def test_weekly_commit_count(self):
10501088
self.response('weekly_commit_count', ETag='"foobarbogus"')
10511089
self.request.return_value.headers['Last-Modified'] = 'foo'
@@ -1094,6 +1132,12 @@ class TestContents(BaseCase):
10941132
def __init__(self, methodName='runTest'):
10951133
super(TestContents, self).__init__(methodName)
10961134
self.contents = repos.contents.Contents(load('readme'))
1135+
self.api = self.contents._api
1136+
1137+
def setUp(self):
1138+
super(TestContents, self).setUp()
1139+
self.contents = repos.contents.Contents(self.contents.to_json(),
1140+
self.g)
10971141

10981142
def test_equality(self):
10991143
contents = repos.contents.Contents(load('readme'))
@@ -1114,6 +1158,47 @@ def test_repr(self):
11141158
def test_str(self):
11151159
expect(str(self.contents)) == self.contents.decoded
11161160

1161+
def test_delete(self):
1162+
self.response('create_content', 200)
1163+
self.delete(self.api)
1164+
self.conf = {
1165+
'data': {
1166+
'message': 'foo',
1167+
'sha': self.contents.sha,
1168+
}
1169+
}
1170+
1171+
with expect.githuberror():
1172+
self.contents.delete(None)
1173+
1174+
self.not_called()
1175+
self.login()
1176+
1177+
c = self.contents.delete('foo')
1178+
expect(c).isinstance(github3.git.Commit)
1179+
self.mock_assertions()
1180+
1181+
def test_update(self):
1182+
self.response('create_content', 200)
1183+
self.put(self.api)
1184+
self.conf = {
1185+
'data': {
1186+
'message': 'foo',
1187+
'content': 'Zm9vIGJhciBib2d1cw==',
1188+
'sha': self.contents.sha,
1189+
}
1190+
}
1191+
1192+
with expect.githuberror():
1193+
self.contents.update(None, None)
1194+
1195+
self.not_called()
1196+
self.login()
1197+
1198+
ret = self.contents.update('foo', b'foo bar bogus')
1199+
expect(ret).isinstance(github3.git.Commit)
1200+
self.mock_assertions()
1201+
11171202

11181203
class TestDownload(BaseCase):
11191204
def __init__(self, methodName='runTest'):

0 commit comments

Comments
 (0)