Skip to content

Commit 5eff8c6

Browse files
authored
Merge pull request sigmavirus24#886 from jacquerie/add-gpg-keys
Add support for GPG keys
2 parents b8e7aa8 + 6506039 commit 5eff8c6

12 files changed

+428
-8
lines changed

src/github3/github.py

+51
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,24 @@ def create_gist(self, description, files, public=True):
335335
json = self._json(self._post(url, data=new_gist), 201)
336336
return self._instance_or_null(gists.Gist, json)
337337

338+
@requires_auth
339+
def create_gpg_key(self, armored_public_key):
340+
"""Create a new GPG key.
341+
342+
.. versionadded:: 1.2.0
343+
344+
:param str armored_public_key:
345+
(required), your GPG key, generated in ASCII-armored format
346+
:returns:
347+
the created GPG key if successful, otherwise ``None``
348+
:rtype:
349+
:class:`~github3.users.GPGKey`
350+
"""
351+
url = self._build_url('user', 'gpg_keys')
352+
data = {'armored_public_key': armored_public_key}
353+
json = self._json(self._post(url, data=data), 201)
354+
return self._instance_or_null(users.GPGKey, json)
355+
338356
@requires_auth
339357
def create_issue(self, owner, repository, title, body=None, assignee=None,
340358
milestone=None, labels=[], assignees=None):
@@ -740,6 +758,39 @@ def gitignore_templates(self):
740758
url = self._build_url('gitignore', 'templates')
741759
return self._json(self._get(url), 200) or []
742760

761+
@requires_auth
762+
def gpg_key(self, id_num):
763+
"""Retrieve the GPG key of the authenticated user specified by id_num.
764+
765+
.. versionadded:: 1.2.0
766+
767+
:returns:
768+
the GPG key specified by id_num
769+
:rtype:
770+
:class:`~github3.users.GPGKey`
771+
"""
772+
url = self._build_url('user', 'gpg_keys', id_num)
773+
json = self._json(self._get(url), 200)
774+
return self._instance_or_null(users.GPGKey, json)
775+
776+
@requires_auth
777+
def gpg_keys(self, number=-1, etag=None):
778+
"""Iterate over the GPG keys of the authenticated user.
779+
780+
.. versionadded:: 1.2.0
781+
782+
:param int number: (optional), number of GPG keys to return. Default:
783+
-1 returns all available GPG keys
784+
:param str etag: (optional), ETag from a previous request to the same
785+
endpoint
786+
:returns:
787+
generator of the GPG keys belonging to the authenticated user
788+
:rtype:
789+
:class:`~github3.users.GPGKey`
790+
"""
791+
url = self._build_url('user', 'gpg_keys')
792+
return self._iter(int(number), url, users.GPGKey, etag=etag)
793+
743794
@requires_auth
744795
def is_following(self, username):
745796
"""Check if the authenticated user is following login.

src/github3/users.py

+149-8
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,103 @@
1212
from .events import Event
1313

1414

15+
class GPGKey(models.GitHubCore):
16+
"""The object representing a user's GPG key.
17+
18+
.. versionadded:: 1.2.0
19+
20+
Please see GitHub's `GPG Key Documentation` for more information.
21+
22+
.. _GPG Key Documentation:
23+
https://developer.github.com/v3/users/gpg_keys/
24+
25+
.. attribute:: can_certify
26+
27+
Whether this GPG key can be used to sign a key.
28+
29+
.. attribute:: can_encrypt_comms
30+
31+
Whether this GPG key can be used to encrypt communications.
32+
33+
.. attribute:: can_encrypt_storage
34+
35+
Whether this GPG key can be used to encrypt storage.
36+
37+
.. attribute:: can_sign
38+
39+
Whether this GPG key can be used to sign some data.
40+
41+
.. attribute:: created_at
42+
43+
A :class:`~datetime.datetime` representing the date and time when
44+
this GPG key was created.
45+
46+
.. attribute:: emails
47+
48+
A list of :class:`~github3.users.ShortEmail` attached to this GPG
49+
key.
50+
51+
.. attribute:: expires_at
52+
53+
A :class:`~datetime.datetime` representing the date and time when
54+
this GPG key will expire.
55+
56+
.. attribute:: id
57+
58+
The unique identifier of this GPG key.
59+
60+
.. attribute:: key_id
61+
62+
A hexadecimal string that identifies this GPG key.
63+
64+
.. attribute:: primary_key_id
65+
66+
The unique identifier of the primary key of this GPG key.
67+
68+
.. attribute:: public_key
69+
70+
The public key contained in this GPG key. This is not a GPG formatted
71+
key, and is not suitable to be used directly in programs like GPG.
72+
73+
.. attribute:: subkeys
74+
75+
A list of :class:`~github3.users.GPGKey` of the subkeys of this GPG
76+
key.
77+
"""
78+
79+
def _update_attributes(self, key):
80+
self.can_certify = key['can_certify']
81+
self.can_encrypt_comms = key['can_encrypt_comms']
82+
self.can_encrypt_storage = key['can_encrypt_storage']
83+
self.can_sign = key['can_sign']
84+
self.created_at = self._strptime(key['created_at'])
85+
self.emails = [ShortEmail(email, self) for email in key['emails']]
86+
self.expires_at = self._strptime(key['expires_at'])
87+
self.id = key['id']
88+
self.key_id = key['key_id']
89+
self.primary_key_id = key['primary_key_id']
90+
self.public_key = key['public_key']
91+
self.subkeys = [GPGKey(subkey, self) for subkey in key['subkeys']]
92+
93+
def _repr(self):
94+
return '<GPG Key [{0}]>'.format(self.key_id)
95+
96+
def __str__(self):
97+
return self.key_id
98+
99+
@requires_auth
100+
def delete(self):
101+
"""Delete this GPG key.
102+
103+
:returns:
104+
True if successful, False otherwise
105+
:rtype:
106+
bool
107+
"""
108+
url = self._build_url('user', 'gpg_keys', self.id)
109+
return self._boolean(self._delete(url), 204, 404)
110+
111+
15112
class Key(models.GitHubCore):
16113
"""The object representing a user's SSH key.
17114
@@ -121,15 +218,26 @@ def is_free(self):
121218
return self.name == 'free' # (No coverage)
122219

123220

124-
class Email(models.GitHubCore):
125-
"""The object used to represent an AuthenticatedUser's email.
221+
class _Email(models.GitHubCore):
222+
"""Base email object."""
126223

127-
Please see GitHub's `Emails documentation`_ for more information.
224+
class_name = '_Email'
128225

129-
.. _Emails documentation:
130-
https://developer.github.com/v3/users/emails/
226+
def _update_attributes(self, email):
227+
self.email = email['email']
228+
self.verified = email['verified']
229+
230+
def _repr(self):
231+
return '<{0} [{1}]>'.format(self.class_name, self.email)
232+
233+
def __str__(self):
234+
return self.email
235+
236+
237+
class ShortEmail(_Email):
238+
"""The object used to represent an email attached to a GPG key.
131239
132-
The attributes represented on this object include:
240+
This object has the following attributes:
133241
134242
.. attribute:: email
135243
@@ -139,16 +247,35 @@ class Email(models.GitHubCore):
139247
140248
A boolean value representing whether the address has been verified or
141249
not
250+
"""
251+
252+
class_name = 'ShortEmail'
253+
254+
255+
class Email(_Email):
256+
"""The object used to represent an AuthenticatedUser's email.
257+
258+
Please see GitHub's `Emails documentation`_ for more information.
259+
260+
.. _Emails documentation:
261+
https://developer.github.com/v3/users/emails/
262+
263+
This object has all of the attributes of :class:`ShortEmail` as well as
264+
the following attributes:
142265
143266
.. attribute:: primary
144267
145268
A boolean value representing whether the address is the primary
146269
address for the user or not
270+
271+
.. attribute:: visibility
272+
273+
A string value representing whether an authenticated user can view the
274+
email address. Use ``public`` to allow it, ``private`` to disallow it.
147275
"""
148276

149277
def _update_attributes(self, email):
150-
self.email = email['email']
151-
self.verified = email['verified']
278+
super(Email, self)._update_attributes(email)
152279
self.primary = email['primary']
153280
self.visibility = email['visibility']
154281

@@ -276,6 +403,20 @@ def following(self, number=-1, etag=None):
276403
url = self._build_url('following', base_url=self._api)
277404
return self._iter(int(number), url, ShortUser, etag=etag)
278405

406+
def gpg_keys(self, number=-1, etag=None):
407+
"""Iterate over the GPG keys of this user.
408+
409+
.. versionadded:: 1.2.0
410+
411+
:param int number: (optional), number of GPG keys to return. Default:
412+
-1 returns all available GPG keys
413+
:param str etag: (optional), ETag from a previous request to the same
414+
endpoint
415+
:returns: generator of :class:`GPGKey <GPGKey>`\ s
416+
"""
417+
url = self._build_url('gpg_keys', base_url=self._api)
418+
return self._iter(int(number), url, GPGKey, etag=etag)
419+
279420
def keys(self, number=-1, etag=None):
280421
r"""Iterate over the public keys of this user.
281422

tests/cassettes/GPGKey_delete.json

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{"http_interactions": [{"request": {"body": {"string": "{\"armored_public_key\": \"-----BEGIN PGP PUBLIC KEY BLOCK-----\\n\\nmI0EW3Gx5AEEAKkl8uAp56B9WlVMRl3ibQN99x/7JAkCWHVU1NjfAa4/AOmhG2Bl\\nFmSCfQ6CBVgOGpdaMtzyq0YxYgvhnhzwwaEZ6mrwz2in1Mo8iOVkXv2eK3ov24PU\\naLoYxiGMtNT8nKQjJLLWrEjrJOnNNGkSUHM8eAVlz3TonZALp0lOsIg/ABEBAAG0\\naUphY29wbyBOb3RhcnN0ZWZhbm8gKENyZWF0ZWQgZm9yIGEgdGVzdCBmb3IgZ2l0\\naHViMy5weSBhbmQgdGhlbiBkZWxldGVkLikgPGphY29wby5ub3RhcnN0ZWZhbm9A\\nZ21haWwuY29tPojOBBMBCgA4FiEEux/Ns2l9RasyufUE8C5SQOx2rKgFAltxseQC\\nGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ8C5SQOx2rKhwEgQApsTrwmfh\\nPgwzX4zPtVvwKq+MYU6idhS2hwouHYPzgsVNOt5P6vW2V9jF9NQrK1gVXMSn1S16\\n6iE/X8R5rkRYbAXlvFnww4xaVCWSrXBhBGDbOCQ4fSuTNEWXREhwHAHnP4nDR+mh\\nmba6f9pMZBZalz8/0jYf2Q2ds5PEhzCQk6K4jQRbcbHkAQQAt9A5ebOFcxFyfxmt\\nOeEkmQArt31U1yATLQQto9AmpQnPk1OHjEsv+4MWaydTnuWKG1sxZb9BQRq8T8ho\\njFcYXg3CAdz2Pi6dA+I6dSKgknVY2qTFURSegFcKOiVJd48oEScMyjnRcn+gDM3Y\\nS3shYhDt1ff6cStm344+HWFyBPcAEQEAAYi2BBgBCgAgFiEEux/Ns2l9RasyufUE\\n8C5SQOx2rKgFAltxseQCGwwACgkQ8C5SQOx2rKhlfgP/dhFe09wMtVE6qXpQAXWU\\nT34sJD7GTcyYCleGtAgbtFD+7j9rk7VTG4hGZlDvW6FMdEQBE18Hd+0UhO1TA0c1\\nXTLKl8sNmIg+Ph3yiED8Nn+ByNk7KqX3SeCNvAFkTZI3yeTAynUmQin68ZqrwMjp\\nIMGmjyjdODb4qOpFvBPAlM8=\\n=2MWr\\n-----END PGP PUBLIC KEY BLOCK-----\"}", "encoding": "utf-8"}, "headers": {"Content-Length": ["1189"], "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 <AUTH_TOKEN>"]}, "method": "POST", "uri": "https://api.github.com/user/gpg_keys"}, "response": {"body": {"string": "{\"id\":414858,\"primary_key_id\":null,\"key_id\":\"F02E5240EC76ACA8\",\"raw_key\":\"-----BEGIN PGP PUBLIC KEY BLOCK-----\\n\\nmI0EW3Gx5AEEAKkl8uAp56B9WlVMRl3ibQN99x/7JAkCWHVU1NjfAa4/AOmhG2Bl\\nFmSCfQ6CBVgOGpdaMtzyq0YxYgvhnhzwwaEZ6mrwz2in1Mo8iOVkXv2eK3ov24PU\\naLoYxiGMtNT8nKQjJLLWrEjrJOnNNGkSUHM8eAVlz3TonZALp0lOsIg/ABEBAAG0\\naUphY29wbyBOb3RhcnN0ZWZhbm8gKENyZWF0ZWQgZm9yIGEgdGVzdCBmb3IgZ2l0\\naHViMy5weSBhbmQgdGhlbiBkZWxldGVkLikgPGphY29wby5ub3RhcnN0ZWZhbm9A\\nZ21haWwuY29tPojOBBMBCgA4FiEEux/Ns2l9RasyufUE8C5SQOx2rKgFAltxseQC\\nGwMFCwkIBwIGFQoJCAsCBBYCAwECHgECF4AACgkQ8C5SQOx2rKhwEgQApsTrwmfh\\nPgwzX4zPtVvwKq+MYU6idhS2hwouHYPzgsVNOt5P6vW2V9jF9NQrK1gVXMSn1S16\\n6iE/X8R5rkRYbAXlvFnww4xaVCWSrXBhBGDbOCQ4fSuTNEWXREhwHAHnP4nDR+mh\\nmba6f9pMZBZalz8/0jYf2Q2ds5PEhzCQk6K4jQRbcbHkAQQAt9A5ebOFcxFyfxmt\\nOeEkmQArt31U1yATLQQto9AmpQnPk1OHjEsv+4MWaydTnuWKG1sxZb9BQRq8T8ho\\njFcYXg3CAdz2Pi6dA+I6dSKgknVY2qTFURSegFcKOiVJd48oEScMyjnRcn+gDM3Y\\nS3shYhDt1ff6cStm344+HWFyBPcAEQEAAYi2BBgBCgAgFiEEux/Ns2l9RasyufUE\\n8C5SQOx2rKgFAltxseQCGwwACgkQ8C5SQOx2rKhlfgP/dhFe09wMtVE6qXpQAXWU\\nT34sJD7GTcyYCleGtAgbtFD+7j9rk7VTG4hGZlDvW6FMdEQBE18Hd+0UhO1TA0c1\\nXTLKl8sNmIg+Ph3yiED8Nn+ByNk7KqX3SeCNvAFkTZI3yeTAynUmQin68ZqrwMjp\\nIMGmjyjdODb4qOpFvBPAlM8=\\n=2MWr\\n-----END PGP PUBLIC KEY BLOCK-----\",\"public_key\":\"xo0EW3Gx5AEEAKkl8uAp56B9WlVMRl3ibQN99x/7JAkCWHVU1NjfAa4/AOmhG2BlFmSCfQ6CBVgOGpdaMtzyq0YxYgvhnhzwwaEZ6mrwz2in1Mo8iOVkXv2eK3ov24PUaLoYxiGMtNT8nKQjJLLWrEjrJOnNNGkSUHM8eAVlz3TonZALp0lOsIg/ABEBAAE=\",\"emails\":[{\"email\":\"jacopo.notarstefano@gmail.com\",\"verified\":true}],\"subkeys\":[{\"id\":414859,\"primary_key_id\":414858,\"key_id\":\"FC1199D5F1EEEE32\",\"raw_key\":null,\"public_key\":\"zo0EW3Gx5AEEALfQOXmzhXMRcn8ZrTnhJJkAK7d9VNcgEy0ELaPQJqUJz5NTh4xLL/uDFmsnU57lihtbMWW/QUEavE/IaIxXGF4NwgHc9j4unQPiOnUioJJ1WNqkxVEUnoBXCjolSXePKBEnDMo50XJ/oAzN2Et7IWIQ7dX3+nErZt+OPh1hcgT3ABEBAAE=\",\"emails\":[],\"subkeys\":[],\"can_sign\":false,\"can_encrypt_comms\":true,\"can_encrypt_storage\":true,\"can_certify\":false,\"created_at\":\"2018-08-18T20:03:09.000+02:00\",\"expires_at\":null}],\"can_sign\":true,\"can_encrypt_comms\":false,\"can_encrypt_storage\":false,\"can_certify\":true,\"created_at\":\"2018-08-18T20:03:09.000+02:00\",\"expires_at\":null}", "encoding": "utf-8"}, "headers": {"Content-Length": ["2146"], "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"], "Access-Control-Allow-Origin": ["*"], "X-Frame-Options": ["deny"], "Status": ["201 Created"], "X-GitHub-Request-Id": ["8C20:07E1:FE0EB2:1CBF9F1:5B785F5D"], "ETag": ["\"5af8c12c884e0c82e9fb6954de058ccd\""], "Date": ["Sat, 18 Aug 2018 18:03:09 GMT"], "X-RateLimit-Remaining": ["4994"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "Server": ["GitHub.com"], "X-OAuth-Scopes": ["admin:gpg_key"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Content-Type-Options": ["nosniff"], "X-Runtime-rack": ["0.050927"], "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": ["admin:gpg_key, write:gpg_key"], "X-RateLimit-Reset": ["1534618941"]}, "status": {"message": "Created", "code": 201}, "url": "https://api.github.com/user/gpg_keys"}, "recorded_at": "2018-08-18T18:03:09"}, {"request": {"body": {"string": "", "encoding": "utf-8"}, "headers": {"Content-Length": ["0"], "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 <AUTH_TOKEN>"]}, "method": "DELETE", "uri": "https://api.github.com/user/gpg_keys/414858"}, "response": {"body": {"string": "", "encoding": null}, "headers": {"Status": ["204 No Content"], "X-RateLimit-Remaining": ["4993"], "X-GitHub-Media-Type": ["github.v3; param=full; format=json"], "X-Runtime-rack": ["0.036032"], "X-Content-Type-Options": ["nosniff"], "Access-Control-Allow-Origin": ["*"], "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"], "X-GitHub-Request-Id": ["8C20:07E1:FE0EC7:1CBFA18:5B785F5D"], "Strict-Transport-Security": ["max-age=31536000; includeSubdomains; preload"], "X-XSS-Protection": ["1; mode=block"], "Server": ["GitHub.com"], "Content-Security-Policy": ["default-src 'none'"], "X-RateLimit-Limit": ["5000"], "Date": ["Sat, 18 Aug 2018 18:03:09 GMT"], "X-OAuth-Scopes": ["admin:gpg_key"], "Referrer-Policy": ["origin-when-cross-origin, strict-origin-when-cross-origin"], "Content-Type": ["application/octet-stream"], "X-Accepted-OAuth-Scopes": ["admin:gpg_key"], "X-Frame-Options": ["deny"], "X-RateLimit-Reset": ["1534618941"]}, "status": {"message": "No Content", "code": 204}, "url": "https://api.github.com/user/gpg_keys/414858"}, "recorded_at": "2018-08-18T18:03:09"}], "recorded_with": "betamax/0.8.1"}

0 commit comments

Comments
 (0)