= Launchpad Web Service = == Prerequisites == * An object being exposed must already support navigation. * It should have a canonical_url, and that canonical_url should lead to it. (See [[https://dev.launchpad.net/Web/URLTraversal|Url Traversal]].) == 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.) * Add the interface to lib/lp//interfaces/webservice.py, so it is discoverable. == 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. == Gotchas == * Due to [[http://launchpad.net/bugs/760849|bug 760849]] exporting web service collections is broken with respect to versioning. You may not specify a version for @export_as_webservice_entry. The consequence is that the items exported by that collection must declare they were exported as of the oldest version, even though that is not the case. Look in the code for XXX comments for bug=760849 for usage examples. == Testing == * Unit tests can be written by extending the `Web``Service``Test``Case`. Look in `soyuz/browser/tests` for examples. == 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 }}} == Complete example branch == This [[https://code.launchpad.net/~bac/launchpad/bug-776437/+merge/65692|merge proposal]] is for a branch that exports from scratch three new classes `I``Processor`, `I``Processor``Family`, and `I``Processor``Family``Set`. None of these classes previously existed in the web UI and none had traversal code. == 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 * Anonymously by appending '''?oauth_token=&oauth_consumer_key=something''' to the URL * With your current cookie by using '''https://launchpad.dev/api/beta/''' instead of ''' https://api.launchpad.dev/beta/'''