Diff for "API/ImplementingAPIs"

Not logged in - Log In / Register

Differences between revisions 4 and 5
Revision 4 as of 2010-03-03 01:47:50
Size: 11591
Editor: adiroiban
Comment:
Revision 5 as of 2010-03-03 01:53:08
Size: 11624
Editor: adiroiban
Comment:
Deletions are marked like this. Additions are marked like this.
Line 31: Line 31:
 * For code and translations, the exported interfaces must be included in lib/lp/<application>/interfaces/webservice.py  * For code and translations (and soon for the other modules), the exported interfaces must be included in lib/lp/<application>/interfaces/webservice.py

Launchpad Web Service

Process Overview

For each content interface that a developer would like to expose on the webservice, a Launchpad developer does the the following:

  • Decide if the object is exported as an "Entry" or as a "Collection". Top-level Set content objects are exposed as collections.
  • Select the attributes that are going to be part of the representation of the object on the web service.
  • For each method that will be exposed on the web service, decide if it represents a read, write, or factory operation. Select the parameters that are to be exported on the web service and define the type of those parameters.
  • Decide if some fields, methods or parameters should be renamed on the webservice (for forward-looking changes based on our new naming conventions.)

Architectural Principles

  • URLs are used as object identifiers. The path to an object on the web service is the same as the equivalent object in the web UI, apart for the hostname and the API version prefix.
  • GET-ting a URL to an object returns a JSON representation of the fields that are part of the object.
  • The mutable fields of the object can be modified by PUT-ting or PATCH-ing to the object URL.
  • Read-only methods can be invoked on the object by GET-ting the object URL with some query parameters. The ws_op parameter contains the name of the method to call and additional query parameters are the actual method call parameters.

  • Methods modifying the object or creating new objects are called by POST-ing to the object's URL. The body of the POST contains the ws_op parameter containing the method to call, and the actual method parameters.

    • Note that we use application/x-www-urlencoded encoding for the POST body for the symmetry it provides with read-only operations where the parameters are urlencoded as part of the query string. This also makes it easy to call a method using any standard HTTP library.

Prerequisites

  • An object being exposed must already support navigation.
  • It should have a canonical_url, and that canonical_url should lead to it.
  • For code and translations (and soon for the other modules), the exported interfaces must be included in lib/lp/<application>/interfaces/webservice.py

Examples

Changing the status of a bug task

In our internal API, a bugtask status can be changed by using the IBugTask.transitionToStatus() methods. To export this method on the web service, the interface is tagged in the following way:

  • class IBugTask(Interface):
        export_as_webservice_entry()
    
        @call_with(user=REQUEST_USER)
        @operation_parameters(
            new_status=Choice(vocabulary=BugTaskStatus, required=True))
        @export_write_operation()
        def transitionToStatus(new_status, user):
            """Perform a workflow transition to the new_status.
    
            :new_status: new status from `BugTaskStatus`
            :user: the user requesting the change
            """

This allows us to change the bugtask status using the following request:

  • POST /beta/ubuntu/+bug/1 HTTP/1.0
    Host: api.lauchpad.net
    Content-Type: application/x-www-form-urlencoded
    Content-Length: 45
    Authorization: OAuth realm="https://api.launchpad.net/",
        oauth_consumer_key="0685bd9184jfhq22",
        oauth_token="ad180jjd733klru7",
        oauth_signature_method="plaintext",
        oauth_signature="",
        oauth_timestamp="137131200",
        oauth_nonce="4572616e48616d6d65724c61686176",
        oauth_version="1.0"
    
    ws_op=transitionToStatus&new_status=INCOMPLETE

Assigning a milestone to a bugtask

The IMilestone and IBugTask interfaces are tagged in the following way to export them on the webservice and specify that the milestone field is mutable.

  • class IMilestone(Interface)
        export_as_webservice_entry()
    
    
    class IBugTask(Interface):
        export_as_webservice_entry()
    
        milestone = Choice(
            title=_('Milestone'), required=False, vocabulary='Milestone')
        export_field(milestone)

The bug task milestone field can then be modified by using the PATCH request:

  • PATCH /beta/ubuntu/+bug/1 HTTP/1.0
    Host: api.launchpad.net
    Content-Type: application/json
    Authorization: ...
    
    {'milestone':
        'https://api.launchpad.net/beta/ubuntu/+milestone/ubuntu-8.04',
    }

Note that multiple attributes could be modified at the same time in the same PATCH request.

Listing the members of a team

Our internal API supports several ways to retrieve members of a team. It offers various attributes retrieving various subset of the members. It also has some methods to query the list of members.

The active members of a team is available in the activemembers attribute. Tagging the IPerson in the following way will allow us to retrieve this list.

  • class IPersonViewRestricted(Interface):
    
        activemembers = CollectionField(
            title=_("List of members with ADMIN or APPROVED status"),
            value_type=Object(schema=IPerson))
        export_field(activemembers, export_as='active_members')

We can then retrieve the active members by using the following request:

  • GET /beta/~launchpad-admins/active_members HTTP/1.0
    Host: api.lauchpad.net
    Authorization: ...

The members are returned in batches:

  • HTTP/1.0 200 Ok
    Content-Type: application/json
    
    {'total_size': 150,
     'ws_start': 0,
     'next_collection_link':
        'https://api.launchpad.net/beta/~launchpad-admins/active_members?ws_start=75&ws_size=75',
     'entries': [
        {'self_link': 'https://api.launchpad.net/beta/~SteveA',
         'name': 'SteveA',
         'display_name': 'Steve Alexander',
         ...},
        {'self_link': 'https://api.launchpad.net/beta/~sabdfl',
         'name': 'sabdfl',
         'display_name': 'Mark Shuttleworth',
        ...},
        ...],
    }

Note here that entries contains the complete representation of each member. This is identical to what you would get by retrieving the 'self_link'.

There is also a getMembersByStatus() method that can be used to retrieve members with a particular status.

This is how the method is tagged in the content interface:

  • class IPersonViewRestricted(Interface):
    
        @operation_parameters(
            status=Choice(vocabulary=TeamMembershipStatus, required=True))
        @export_read_operation()
        def getMembersByStatus(status, orderby=None):
            """Return the people whose membership on this team match :status:.
    
            If no orderby is provided, Person.sortingColumns is used.
            """

This allows us to call the operation using the following request:

  • GET /beta/~launchpad-admins?ws_op=getMembersByStatus&status=APPROVED
    Host: api.launchpad.net
    Authorization: ...

Which will give us also a batch result set, but the next batch URLs point back to the method call this time.

  • HTTP/1.0 200 Ok
    Content-Type: application/json
    
    {'total_size': 150,
     'ws_start': 0,
     'next_collection_link':
        'https://api.launchpad.net/beta/~launchpad-admins?ws_op=getMembersByStatus&status=APPROVED&ws_start=75&ws_size=75',
     'entries': [
        {'self_link': 'https://api.launchpad.net/beta/~SteveA',
         'name': 'SteveA',
         'display_name': 'Steve Alexander',
         ...},
        {'self_link': 'https://api.launchpad.net/beta/~sabdfl',
         'name': 'sabdfl',
         'display_name': 'Mark Shuttleworth',
        ...},
        ...
     ]
    }

Adding a member to the team

A team administrator can add a person to the team by using the addMember() operation. This how to tag the method in the content interface to allow this:

  • class IPersonEditRestricted(Interface):
    
        @call_with(reviewer=REQUEST_USER)
        @operation_parameters(
            person=Choice(required=True, vocabulary='ValidPersonOrTeam'))
        @export_write_operation()
        def addMember(person, reviewer, status=TeamMembershipStatus.APPROVED,
                      comment=None, force_team_add=False):
            """Add the given person as a member of this team."""

This allows us to add a user to the field by doing the following web request:

  • POST /beta/~launchpad-admins HTTP/1.0
    Host: api.lauchpad.net
    Content-Type: application/x-www-form-urlencoded
    Authorization: ...
    
    ws_op=addMember&person=https%3A//api.launchpad.net/beta/%7Eherb

Registering a new project

Projects are created using the IProductSet.createProduct() method. Here is how the method should be tagged for export on the web site. This example also demonstrates how to rename exported names on the web service for forward-looking reasons.

  • class IProductSet(Interface):
        export_as_webservice_collection()
    
    
        @export_operation_as('create_product')
        @rename_parameters_as(displayname='display_name')
        @call_with(owner=REQUEST_USER)
        @export_factory_operation(
            IProduct, fields=['name', 'displayname', 'title', 'summary',
            'description', 'licenses', 'license_info'])
        def createProduct(owner, name, displayname, title, summary,
                          description, project=None, homepageurl=None,
                          screenshotsurl=None, wikiurl=None,
                          downloadurl=None, freshmeatproject=None,
                          sourceforgeproject=None, programminglang=None,
                          reviewed=False, mugshot=None, logo=None,
                          icon=None, licenses=(), license_info=None):
            """Create and Return a brand new Product."""

A new product can then be created using the following request:

  • POST /projects HTTP/1.0
    Host: api.lauchpad.net
    Content-Type: application/x-www-form-urlencoded
    Authorization: ...
    
    ws_op=create_product&name=bzr-metrics&display_name=Software+Engineering+Process+Metrics+BZR+Plugin&title=&summary=&description=

We can see in the response, the main difference between a regular 'write_operation' and a 'factory_operation'. In the latter, instead of replying with the generic 200 response code, the server uses 201 and reports the URL of the newly created project.

  • HTTP/1.0 201 Created
    Location: https://api.launchpad.net/bzr-metrics

Don't repeat yourself

We are also designing an improvement to how we declare parameter types so that we can say "the parameter status has the same type as Membership.status".

Error handling

Errors are reported using regular HTTP status code. So for example, here are the replies that would be returned in the following conditions:

  • When a parameter is missing or has an invalid value:
    • HTTP/1.0 400 Bad Request
      Content-Type: text/plain
      
      status: Required input is missing.
      assignee: Should be a person.
  • When some arbitrary pre-conditions wasn't met:
    • HTTP/1.0 400 Bad Request
      Content-Type: text/plain
      
      status: AssertionError: only Bug Supervisors can set the status to TRIAGED.
  • When the user doesn't have permission to invoke an operation:
    • HTTP/1.0 403 Forbidden

Some tips and tricks

* to update the lp.dev/+apidoc/index.html and the wadl file you must first manualy delete them

  •     rm ./lib/canonical/launchpad/apidoc/*
        make build

* You can access the API from a web browser in the following ways

API/ImplementingAPIs (last edited 2011-06-23 17:57:25 by bac)