Merge lp:~didrocks/launchpad/expose-sshkeys-bug-357235 into lp:launchpad/db-devel

Proposed by Didier Roche-Tolomelli
Status: Superseded
Proposed branch: lp:~didrocks/launchpad/expose-sshkeys-bug-357235
Merge into: lp:launchpad/db-devel
Diff against target: 197 lines (+99/-8) (has conflicts)
5 files modified
lib/lp/registry/browser/configure.zcml (+8/-0)
lib/lp/registry/browser/tests/test_sshkey.py (+31/-0)
lib/lp/registry/interfaces/person.py (+8/-3)
lib/lp/registry/interfaces/ssh.py (+10/-5)
lib/lp/registry/stories/webservice/xx-person.txt (+42/-0)
Text conflict in lib/lp/registry/browser/configure.zcml
To merge this branch: bzr merge lp:~didrocks/launchpad/expose-sshkeys-bug-357235
Reviewer Review Type Date Requested Status
Francis J. Lacoste (community) Needs Fixing
Graham Binns code Pending
Review via email: mp+20995@code.launchpad.net

This proposal supersedes a proposal from 2010-03-09.

This proposal has been superseded by a proposal from 2010-03-24.

To post a comment you must log in.
Revision history for this message
Graham Binns (gmb) wrote : Posted in a previous version of this proposal

Hi Didier,

I'm not comfortable reviewing this branch. There's no description of the change in the merge proposal description and I don't know whether you've had a pre-implementation discussion about the branch.

You need to include the following items when you submit a Launchpad branch for review:

 * A description of the change, including a brief list of changes by file
 * The output of `make lint`, run in the root of your branch.
 * Details of the person with whom you had a pre implementation discussion, including (if necessary) details of why you chose the solution you did.

I'm going to reject this branch; please resubmit it with the above items included. If you want to have a pre-implementation discussion this afternoon I'll be happy to make myself available.

review: Needs Resubmitting
Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote : Posted in a previous version of this proposal
Download full text (6.3 KiB)

The branch has been made in a pair programming session with jml, that's why I didn't added any more detail about it.
You can see inspiration and pair session programming on jml's gpg branch: http://bazaar.launchpad.net/~jml/launchpad/expose-gpgkeys-bug-389872/

The change is for exposing ssh key to the API needed for Quickly. We don't allow uploading or setting it directly yet (this will be an UDS discussion).

https://bugs.edge.launchpad.net/launchpad-registry/+bug/357235 is for the corresponding bug report. Again, we don't upload the ssh key/setting is right now.

output of make lint, I added also the testsuite call too:
$ make lint
utilities/shhh.py PYTHONPATH= python2.5 bootstrap.py\
                --ez_setup-source=ez_setup.py \
  --download-base=download-cache/dist --eggs=eggs
Enter passphrase for key '/home/ubuntu/.ssh/id_rsa':
= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/tests/test_sshkey.py
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/interfaces/ssh.py
  lib/lp/registry/stories/webservice/xx-person.txt

== Pyflakes notices ==

lib/lp/registry/interfaces/ssh.py
    19: 'export_read_operation' imported but unused
    19: 'export_as_webservice_collection' imported but unused
    19: 'operation_parameters' imported but unused
    19: 'collection_default_content' imported but unused
    19: 'operation_returns_collection_of' imported but unused

== Pylint notices ==

lib/lp/registry/interfaces/person.py
    520: [C0301] Line too long (80/78)
    53: [F0401] Unable to import 'lazr.enum' (No module named enum)
    54: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    55: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    56: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    63: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    410: [E1002, PersonNameField._validate] Use super on an old style class
    1404: [C0322, IPersonEditRestricted.addMember] Operator not preceded by a space
    status=copy_field(ITeamMembership['status']),
    ^
    comment=Text(required=False))
    @export_write_operation()
    def addMember(person, reviewer, status=TeamMembershipStatus.APPROVED,
    comment=None, force_team_add=False,
    may_subscribe_to_list=True):
    1445: [C0322, IPersonEditRestricted.acceptInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def acceptInvitationToBeMemberOf(team, comment):
    1457: [C0322, IPersonEditRestricted.declineInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def declineInvitationToBeMemberOf(team, comment):
    1755: [C0322, IPersonSet.newTeam] Operator not preceded by a space
    defaultmembershipperiod='default_membership_period',
    ^
    defaultrenewalperiod='default_renewal_period')
    @operation_parameters(
    subscriptionpolicy=Choice(
    title=_('Subs...

Read more...

Revision history for this message
Graham Binns (gmb) wrote : Posted in a previous version of this proposal

Hi Didier, thanks for adding the details.

== Pyflakes notices ==

> lib/lp/registry/interfaces/ssh.py
> 19: 'export_read_operation' imported but unused
> 19: 'export_as_webservice_collection' imported but unused
> 19: 'operation_parameters' imported but unused
> 19: 'collection_default_content' imported but unused
> 19: 'operation_returns_collection_of' imported but unused
>
> == Pylint notices ==
>
> lib/lp/registry/interfaces/person.py
> 520: [C0301] Line too long (80/78)

You can fix these easily enough. You can pretty much ignore the rest;
Pylint produces a lot of noise and should be taken out and shot.

Other than that I'm happy with this branch. Let me know if you want me
to land it for you (I don't know whether it needs to be landed with
jml's work or not).

review: Approve (code)
Revision history for this message
Francis J. Lacoste (flacoste) wrote : Posted in a previous version of this proposal

On March 9, 2010, Didier Roche wrote:
> class ISSHKey(Interface):
> """SSH public key"""
> - id = Int(title=_("Database ID"), required=True, readonly=True)
> +
> + export_as_webservice_entry('ssh_key')
> +
> + id = exported(Int(title=_("Database ID"), required=True,
> readonly=True)) person = Int(title=_("Owner"), required=True,
> readonly=True)
> personID = Int(title=_('Owner ID'), required=True, readonly=True)
> - keytype = Choice(title=_("Key type"), required=True,
> - vocabulary=SSHKeyType)
> - keytext = TextLine(title=_("Key text"), required=True)
> - comment = TextLine(title=_("Comment describing this key"),
> - required=True)
> + keytype = exported(Choice(title=_("Key type"), required=True,
> + vocabulary=SSHKeyType))
> + keytext = exported(TextLine(title=_("Key text"), required=True))
> + comment = exported(TextLine(title=_("Comment describing this key"),
> + required=True))

These fields should all be exported as readonly=True.

--
Francis J. Lacoste
<email address hidden>

Revision history for this message
Francis J. Lacoste (flacoste) : Posted in a previous version of this proposal
review: Needs Fixing
Revision history for this message
Didier Roche-Tolomelli (didrocks) wrote :
Download full text (3.5 KiB)

ok, those are fixed now. (readonly, uneeded import and too long lines). testsuite is still ok and here is the new make lint output:

$ make lint
utilities/shhh.py PYTHONPATH= python2.5 bootstrap.py\
                --ez_setup-source=ez_setup.py \
  --download-base=download-cache/dist --eggs=eggs
Enter passphrase for key '/home/ubuntu/.ssh/id_rsa':
Enter passphrase for key '/home/ubuntu/.ssh/id_rsa':
= Launchpad lint =

Checking for conflicts. and issues in doctests and templates.
Running jslint, xmllint, pyflakes, and pylint.
Using normal rules.

Linting changed files:
  lib/lp/registry/browser/configure.zcml
  lib/lp/registry/browser/tests/test_sshkey.py
  lib/lp/registry/interfaces/person.py
  lib/lp/registry/interfaces/ssh.py
  lib/lp/registry/stories/webservice/xx-person.txt

== Pylint notices ==

lib/lp/registry/interfaces/person.py
    53: [F0401] Unable to import 'lazr.enum' (No module named enum)
    54: [F0401] Unable to import 'lazr.lifecycle.snapshot' (No module named lifecycle)
    55: [F0401] Unable to import 'lazr.restful.interface' (No module named restful)
    56: [F0401] Unable to import 'lazr.restful.declarations' (No module named restful)
    63: [F0401] Unable to import 'lazr.restful.fields' (No module named restful)
    410: [E1002, PersonNameField._validate] Use super on an old style class
    1404: [C0322, IPersonEditRestricted.addMember] Operator not preceded by a space
    status=copy_field(ITeamMembership['status']),
    ^
    comment=Text(required=False))
    @export_write_operation()
    def addMember(person, reviewer, status=TeamMembershipStatus.APPROVED,
    comment=None, force_team_add=False,
    may_subscribe_to_list=True):
    1445: [C0322, IPersonEditRestricted.acceptInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def acceptInvitationToBeMemberOf(team, comment):
    1457: [C0322, IPersonEditRestricted.declineInvitationToBeMemberOf] Operator not preceded by a space
    comment=Text())
    ^
    @export_write_operation()
    def declineInvitationToBeMemberOf(team, comment):
    1755: [C0322, IPersonSet.newTeam] Operator not preceded by a space
    defaultmembershipperiod='default_membership_period',
    ^
    defaultrenewalperiod='default_renewal_period')
    @operation_parameters(
    subscriptionpolicy=Choice(
    title=_('Subscription policy'), vocabulary=TeamSubscriptionPolicy,
    required=False, default=TeamSubscriptionPolicy.MODERATED))
    @export_factory_operation(
    ITeam, ['name', 'displayname', 'teamdescription',
    'defaultmembershipperiod', 'defaultrenewalperiod'])
    def newTeam(teamowner, name, displayname, teamdescription=None,
    subscriptionpolicy=TeamSubscriptionPolicy.MODERATED,
    defaultmembershipperiod=None, defaultrenewalperiod=None):
    1824: [C0322, IPersonSet.findPerson] Operator not preceded by a space
    created_after=Datetime(
    ^
    title=_("Created after"), required=False),
    created_before=Datetime(
    title=_("Created before"), required=False),
    )
    @operation_returns_collection_of(IPerson)
    @export_read_operation()
    def findPerson(text="", exclude_inactive_accounts=True,
    must...

Read more...

Revision history for this message
Karl Fogel (kfogel) wrote :

I'm not a reviewer, but this looks pretty easy to review (and at least on the surface the change seems correct to me). The formatting fix at @@ -519,8 +520,8 @@ is unrelated -- personally, I find those distracting in a branch containing functional changes, but YMMV.

Revision history for this message
Francis J. Lacoste (flacoste) wrote :
Download full text (5.9 KiB)

Your test is not working yet, but we are getting there!

> === modified file 'lib/lp/registry/browser/configure.zcml'
> --- lib/lp/registry/browser/configure.zcml 2010-03-08 01:51:58 +0000
> +++ lib/lp/registry/browser/configure.zcml 2010-03-09 19:30:49 +0000
> @@ -2172,4 +2172,9 @@
> classes="
> PersonProductFacets"
> module="lp.registry.browser.personproduct"/>
> + <browser:url
> + for="lp.registry.interfaces.ssh.ISSHKey"
> + path_expression="string:+ssh-keys/${id}"
> + rootsite="api"
> + attribute_to_parent="person" />
> </configure>

We usually refrain from exposing DB id publically. In URLs, especially. We
have a couple of exception and since there is no alternative here, I think
it's fine.

> === modified file 'lib/lp/registry/interfaces/person.py'
> --- lib/lp/registry/interfaces/person.py 2010-03-05 14:50:47 +0000

>
> oauth_request_tokens = Attribute(_("Non-expired request tokens"))
>
> - sshkeys = Attribute(_('List of SSH keys'))
> + sshkeys = exported(
> + CollectionField(
> + title= _('List of SSH keys'),
> + readonly=False, required=False,
> + value_type=Reference(schema=ISSHKey)))

Can we rename that to ssh_keys? Unfortunately, you can't use exported_as
because of bug 546324. I think there isn't that many call sites, but feel free
to push back if this is too daunting a task.

> === modified file 'lib/lp/registry/interfaces/ssh.py'
> --- lib/lp/registry/interfaces/ssh.py 2009-06-25 04:06:00 +0000
> +++ lib/lp/registry/interfaces/ssh.py 2010-03-09 19:30:49 +0000
> @@ -16,6 +16,7 @@
> from zope.schema import Choice, Int, TextLine
> from zope.interface import Interface
> from lazr.enum import DBEnumeratedType, DBItem
> +from lazr.restful.declarations import (export_as_webservice_entry, exported)
>
> from canonical.launchpad import _
>
> @@ -42,14 +43,18 @@
>
> class ISSHKey(Interface):
> """SSH public key"""
> - id = Int(title=_("Database ID"), required=True, readonly=True)
> +
> + export_as_webservice_entry('ssh_key')
> +
> + id = exported(Int(title=_("Database ID"), required=True, readonly=True))
> person = Int(title=_("Owner"), required=True, readonly=True)

We don't want to export the DB id. It's not useful at all. I know it's leaked
into the URL, but that's an artefact.

> personID = Int(title=_('Owner ID'), required=True, readonly=True)
> - keytype = Choice(title=_("Key type"), required=True,
> - vocabulary=SSHKeyType)
> - keytext = TextLine(title=_("Key text"), required=True)
> - comment = TextLine(title=_("Comment describing this key"),
> - required=True)
> + keytype = exported(Choice(title=_("Key type"), required=True,
> + vocabulary=SSHKeyType, readonly=True))
> + keytext = exported(TextLine(title=_("Key text"), required=True,
> + readonly=True))
> + comment = exported(TextLine(title=_("Comment describing this key"),
> + required=True, readonly=True))

> === modified file 'lib/lp/registry/stories/webservice/xx-person.txt'

> +== SSH keys ===
> +
> +People have SSH keys which we can manipulate over the API.
> +
> +The sample person "name12" doesn't have any keys to begin with:
> +
> + ...

Read more...

review: Needs Fixing

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'lib/lp/registry/browser/configure.zcml'
--- lib/lp/registry/browser/configure.zcml 2010-03-11 16:48:37 +0000
+++ lib/lp/registry/browser/configure.zcml 2010-03-24 21:01:36 +0000
@@ -2172,6 +2172,7 @@
2172 classes="2172 classes="
2173 PersonProductFacets"2173 PersonProductFacets"
2174 module="lp.registry.browser.personproduct"/>2174 module="lp.registry.browser.personproduct"/>
2175<<<<<<< TREE
21752176
2176 <browser:url2177 <browser:url
2177 for="lp.registry.interfaces.gpg.IGPGKey"2178 for="lp.registry.interfaces.gpg.IGPGKey"
@@ -2179,4 +2180,11 @@
2179 rootsite="api"2180 rootsite="api"
2180 attribute_to_parent="owner" />2181 attribute_to_parent="owner" />
21812182
2183=======
2184 <browser:url
2185 for="lp.registry.interfaces.ssh.ISSHKey"
2186 path_expression="string:+ssh-keys/${id}"
2187 rootsite="api"
2188 attribute_to_parent="person" />
2189>>>>>>> MERGE-SOURCE
2182</configure>2190</configure>
21832191
=== added file 'lib/lp/registry/browser/tests/test_sshkey.py'
--- lib/lp/registry/browser/tests/test_sshkey.py 1970-01-01 00:00:00 +0000
+++ lib/lp/registry/browser/tests/test_sshkey.py 2010-03-24 21:01:36 +0000
@@ -0,0 +1,31 @@
1# Copyright 2010 Canonical Ltd. This software is licensed under the
2# GNU Affero General Public License version 3 (see the file LICENSE).
3
4"""Tests for GPG key on the web."""
5
6__metaclass__ = type
7
8import unittest
9
10from canonical.launchpad.webapp import canonical_url
11from canonical.testing.layers import DatabaseFunctionalLayer
12from lp.testing import TestCaseWithFactory
13
14
15class TestCanonicalUrl(TestCaseWithFactory):
16
17 layer = DatabaseFunctionalLayer
18
19 def test_canonical_url(self):
20 # The canonical URL of a GPG key is ssh-keys
21 person = self.factory.makePerson()
22 sshkey = self.factory.makeSSHKey(person)
23 self.assertEqual(
24 '%s/+ssh-keys/%s' % (
25 canonical_url(person, rootsite='api'), sshkey.id),
26 canonical_url(sshkey))
27
28
29def test_suite():
30 return unittest.TestLoader().loadTestsFromName(__name__)
31
032
=== modified file 'lib/lp/registry/interfaces/person.py'
--- lib/lp/registry/interfaces/person.py 2010-03-11 20:59:17 +0000
+++ lib/lp/registry/interfaces/person.py 2010-03-24 21:01:36 +0000
@@ -94,6 +94,7 @@
94from lp.registry.interfaces.mailinglistsubscription import (94from lp.registry.interfaces.mailinglistsubscription import (
95 MailingListAutoSubscribePolicy)95 MailingListAutoSubscribePolicy)
96from lp.registry.interfaces.mentoringoffer import IHasMentoringOffers96from lp.registry.interfaces.mentoringoffer import IHasMentoringOffers
97from lp.registry.interfaces.ssh import ISSHKey
97from lp.registry.interfaces.teammembership import (98from lp.registry.interfaces.teammembership import (
98 ITeamMembership, ITeamParticipation, TeamMembershipStatus)99 ITeamMembership, ITeamParticipation, TeamMembershipStatus)
99from lp.registry.interfaces.wikiname import IWikiName100from lp.registry.interfaces.wikiname import IWikiName
@@ -520,8 +521,8 @@
520 description=_(521 description=_(
521 "An image of exactly 64x64 pixels that will be displayed in "522 "An image of exactly 64x64 pixels that will be displayed in "
522 "the heading of all pages related to you. Traditionally this "523 "the heading of all pages related to you. Traditionally this "
523 "is a logo, a small picture or a personal mascot. It should be "524 "is a logo, a small picture or a personal mascot. It should "
524 "no bigger than 50kb in size.")))525 "be no bigger than 50kb in size.")))
525 logoID = Int(title=_('Logo ID'), required=True, readonly=True)526 logoID = Int(title=_('Logo ID'), required=True, readonly=True)
526527
527 mugshot = exported(MugshotImageUpload(528 mugshot = exported(MugshotImageUpload(
@@ -606,7 +607,11 @@
606607
607 oauth_request_tokens = Attribute(_("Non-expired request tokens"))608 oauth_request_tokens = Attribute(_("Non-expired request tokens"))
608609
609 sshkeys = Attribute(_('List of SSH keys'))610 sshkeys = exported(
611 CollectionField(
612 title= _('List of SSH keys'),
613 readonly=False, required=False,
614 value_type=Reference(schema=ISSHKey)))
610615
611 account_status = Choice(616 account_status = Choice(
612 title=_("The status of this person's account"), required=False,617 title=_("The status of this person's account"), required=False,
613618
=== modified file 'lib/lp/registry/interfaces/ssh.py'
--- lib/lp/registry/interfaces/ssh.py 2009-06-25 04:06:00 +0000
+++ lib/lp/registry/interfaces/ssh.py 2010-03-24 21:01:36 +0000
@@ -16,6 +16,7 @@
16from zope.schema import Choice, Int, TextLine16from zope.schema import Choice, Int, TextLine
17from zope.interface import Interface17from zope.interface import Interface
18from lazr.enum import DBEnumeratedType, DBItem18from lazr.enum import DBEnumeratedType, DBItem
19from lazr.restful.declarations import (export_as_webservice_entry, exported)
1920
20from canonical.launchpad import _21from canonical.launchpad import _
2122
@@ -42,14 +43,18 @@
4243
43class ISSHKey(Interface):44class ISSHKey(Interface):
44 """SSH public key"""45 """SSH public key"""
46
47 export_as_webservice_entry('ssh_key')
48
45 id = Int(title=_("Database ID"), required=True, readonly=True)49 id = Int(title=_("Database ID"), required=True, readonly=True)
46 person = Int(title=_("Owner"), required=True, readonly=True)50 person = Int(title=_("Owner"), required=True, readonly=True)
47 personID = Int(title=_('Owner ID'), required=True, readonly=True)51 personID = Int(title=_('Owner ID'), required=True, readonly=True)
48 keytype = Choice(title=_("Key type"), required=True,52 keytype = exported(Choice(title=_("Key type"), required=True,
49 vocabulary=SSHKeyType)53 vocabulary=SSHKeyType, readonly=True))
50 keytext = TextLine(title=_("Key text"), required=True)54 keytext = exported(TextLine(title=_("Key text"), required=True,
51 comment = TextLine(title=_("Comment describing this key"),55 readonly=True))
52 required=True)56 comment = exported(TextLine(title=_("Comment describing this key"),
57 required=True, readonly=True))
5358
54 def destroySelf():59 def destroySelf():
55 """Remove this SSHKey from the database."""60 """Remove this SSHKey from the database."""
5661
=== modified file 'lib/lp/registry/stories/webservice/xx-person.txt'
--- lib/lp/registry/stories/webservice/xx-person.txt 2010-03-13 00:32:40 +0000
+++ lib/lp/registry/stories/webservice/xx-person.txt 2010-03-24 21:01:36 +0000
@@ -46,6 +46,7 @@
46 proposed_members_collection_link: u'http://.../~salgado/proposed_members'46 proposed_members_collection_link: u'http://.../~salgado/proposed_members'
47 resource_type_link: u'http://.../#person'47 resource_type_link: u'http://.../#person'
48 self_link: u'http://.../~salgado'48 self_link: u'http://.../~salgado'
49 sshkeys_collection_link: u'http://.../~salgado/sshkeys'
49 sub_teams_collection_link: u'http://.../~salgado/sub_teams'50 sub_teams_collection_link: u'http://.../~salgado/sub_teams'
50 super_teams_collection_link: u'http://.../~salgado/super_teams'51 super_teams_collection_link: u'http://.../~salgado/super_teams'
51 team_owner_link: None52 team_owner_link: None
@@ -96,6 +97,7 @@
96 renewal_policy: u'invite them to apply for renewal'97 renewal_policy: u'invite them to apply for renewal'
97 resource_type_link: u'http://.../#team'98 resource_type_link: u'http://.../#team'
98 self_link: u'http://.../~ubuntu-team'99 self_link: u'http://.../~ubuntu-team'
100 sshkeys_collection_link: u'http://.../~ubuntu-team/sshkeys'
99 sub_teams_collection_link: u'http://.../~ubuntu-team/sub_teams'101 sub_teams_collection_link: u'http://.../~ubuntu-team/sub_teams'
100 subscription_policy: u'Moderated Team'102 subscription_policy: u'Moderated Team'
101 super_teams_collection_link: u'http://.../~ubuntu-team/super_teams'103 super_teams_collection_link: u'http://.../~ubuntu-team/super_teams'
@@ -151,6 +153,46 @@
151 HTTP/1.1 404 Not Found153 HTTP/1.1 404 Not Found
152 ...154 ...
153155
156== SSH keys ===
157
158People have SSH keys which we can manipulate over the API.
159
160The sample person "ssh-user" doesn't have any keys to begin with:
161
162 >>> login('test@canonical.com')
163 >>> person = factory.makePerson(name="ssh-user")
164 >>> logout()
165 >>> sample_person = webservice.get("/~ssh-user").jsonBody()
166 >>> sshkeys = sample_person['sshkeys_collection_link']
167 >>> print sshkeys
168 http://.../~ssh-user/sshkeys
169 >>> print_self_link_of_entries(webservice.get(sshkeys).jsonBody())
170
171Let's give "ssh-user" a key via the back door of our internal Python APIs:
172
173 >>> from zope.component import getUtility
174 >>> from lp.registry.interfaces.person import IPersonSet
175 >>> login(ANONYMOUS)
176 >>> ssh_user = getUtility(IPersonSet).getByName('ssh-user')
177 >>> ssh_key = factory.makeSSHKey(ssh_user)
178 >>> logout()
179
180Now when we get the sshkey collection for 'sssh-user' again, the key should show
181up:
182
183 >>> keys = webservice.get(sshkeys).jsonBody()
184 >>> print_self_link_of_entries(keys)
185 http://.../~ssh-user/+ssh-keys/...
186
187
188And then we can actually retrieve the key:
189
190 >>> pprint_entry(keys['entries'][0])
191 comment: u'generic-string...'
192 keytext: u'generic-string...'
193 keytype: u'RSA'
194 resource_type_link: u'http://.../#ssh_key'
195 self_link: u'http://.../~ssh-user/+ssh-keys/...'
154196
155=== GPG keys ===197=== GPG keys ===
156198

Subscribers

People subscribed via source and target branches

to status/vote changes: