Skip to content
This repository has been archived by the owner on Mar 28, 2019. It is now read-only.

Implement batch api #93

Merged
merged 11 commits into from
Feb 6, 2015
1 change: 1 addition & 0 deletions config/readinglist.ini
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ readinglist.eos =
# readinglist.basic_auth_backdoor = true
# readinglist.backoff = 10
readinglist.userid_hmac_secret = b4c96a8692291d88fe5a97dd91846eb4
# readinglist.batch_max_requests = 25

fxa-oauth.client_id =
fxa-oauth.client_secret =
Expand Down
122 changes: 122 additions & 0 deletions docs/batch.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
################
Batch operations
################

.. _batch:

POST /batch
===========

**Requires an FxA OAuth authentication**

The POST body is a mapping, with the following attributes:

- ``requests``: the list of requests
- ``defaults``: (*optional*) values in common for all requests
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While reading the code (without more information), I found it hard to unsderstand what this was for. We probably should rename this argument to default_values or default_request_values, that would make it clearer.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or request_defaults

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not a fan to be honest :( defaults is fine IMO :)

``defaults``: (*optional*) default values for attributes of every batch request

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reading the docs it's fine, but the name of the argument doesn't convey much more meaning, and I had to read the docs to understand what the purpose was.


Each request is a JSON mapping, with the following attribute:

- ``method``: HTTP verb
- ``path``: URI
- ``body``: a mapping
- ``headers``: (*optional*), otherwise take those of batch request

::

{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we provide examples, we should do it with HTTPie so that it's easier for clients to reproduce.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will be adressed in #101

"defaults": {
"method" : "POST",
"path" : "/articles",
"headers" : {
...
}
},
"requests": [
{
"body" : {
"title": "MoFo",
"url" : "http://mozilla.org"
}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: update this to put body as string

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a requirements? It seems strange to me to have JSON encoded string inside a JSON. I rather prefer to have a nested object instead.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. But we should support both then (suppose we want to support xml or form encoded payloads)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Well we don't :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I concur here: we currently don't and in case we want to, we can re-assess this formalism to include arbitrary strings.

},
{
"body" : {
"title": "MoCo",
"url" : "http://mozilla.com"
}
},
{
"method" : "PATCH",
"path" : "/articles/409",
"body" : {
"read_position" : 3477
}
}
]
]


The response body is a list of all responses:

::

{
"responses": [
{
"path" : "/articles/409",
"status": 200,
"body" : {
"id": 409,
"url": "...",
...
"read_position" : 3477
},
"headers": {
...
}
},
{
"status": 201,
"path" : "/articles",
"body" : {
"id": 411,
"title": "MoFo",
"url" : "http://mozilla.org",
...
},
},
{
"status": 201,
"path" : "/articles",
"body" : {
"id": 412,
"title": "MoCo",
"url" : "http://mozilla.com",
...
},
},
]
]


:warning:

Since the requests bodies are necessarily mappings, posting arbitrary data
(*like raw text or binary*)is not supported.

:note:

The responses are in the same order of the requests.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't believe this is english enough :)

Responses are provided in the same order than requests



Pros & Cons
:::::::::::

* This respects REST principles
* This is easy for the client to handle, since it just has to pile up HTTP requests while offline
* It looks to be a convention for several REST APIs (`Neo4J <http://neo4j.com/docs/milestone/rest-api-batch-ops.html>`_, `Facebook <https://developers.facebook.com/docs/graph-api/making-multiple-requests>`_, `Parse <ttps://parse.com/docs/rest#objects-batch>`_)
* Payload of response can be heavy, especially while importing huge collections
* Payload of response must all be iterated to look-up errors

:note:

A form of payload optimization for massive operations is planned.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Contents:
model
authentication
api
batch
utilities
timestamps
versionning
Expand Down
21 changes: 19 additions & 2 deletions readinglist/errors.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import six
from pyramid.config import global_registries
from pyramid.httpexceptions import (
HTTPServiceUnavailable as PyramidHTTPServiceUnavailable, HTTPBadRequest
HTTPServiceUnavailable as PyramidHTTPServiceUnavailable, HTTPBadRequest,
HTTPInternalServerError as PyramidHTTPInternalServerError
)
from readinglist.utils import Enum, json

Expand Down Expand Up @@ -41,7 +42,7 @@ def format_error(code, errno, error, message=None, info=None):


class HTTPServiceUnavailable(PyramidHTTPServiceUnavailable):
"""Return an HTTPServiceUnavailable formatted error."""
"""A HTTPServiceUnavailable formatted in JSON."""

def __init__(self, **kwargs):
if 'body' not in kwargs:
Expand All @@ -65,6 +66,22 @@ def __init__(self, **kwargs):
super(HTTPServiceUnavailable, self).__init__(**kwargs)


class HTTPInternalServerError(PyramidHTTPInternalServerError):
"""A HTTPInternalServerError formatted in JSON."""

def __init__(self, **kwargs):
kwargs.setdefault('body', format_error(
500,
ERRORS.UNDEFINED,
"Internal Server Error",
"A programmatic error occured, developers have been informed.",
"https://github.com/mozilla-services/readinglist/issues/"))

kwargs.setdefault('content_type', 'application/json')

super(HTTPInternalServerError, self).__init__(**kwargs)


def json_error(errors):
"""Return an HTTPError with the given status and message.

Expand Down
13 changes: 2 additions & 11 deletions readinglist/tests/resource/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,5 @@
import mock

from cornice import errors as cornice_errors

from readinglist.backend.memory import Memory
from readinglist.tests.support import unittest
from readinglist.tests.support import unittest, DummyRequest
from readinglist.resource import BaseResource


Expand All @@ -16,13 +12,8 @@ def setUp(self):
self.resource = BaseResource(self.get_request())

def get_request(self):
request = mock.MagicMock(headers={})
request = DummyRequest()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Miam 👍

request.db = self.db
request.errors = cornice_errors.Errors()
request.authenticated_userid = 'bob'
request.validated = {}
request.matchdict = {}
request.response = mock.MagicMock(headers={})
return request

@property
Expand Down
16 changes: 15 additions & 1 deletion readinglist/tests/support.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import mock
import threading
import webtest

try:
import unittest2 as unittest
except ImportError:
import unittest # NOQA

from cornice import errors as cornice_errors
import webtest

from readinglist import API_VERSION
from readinglist.utils import random_bytes_hex


class DummyRequest(mock.MagicMock):
def __init__(self, *args, **kwargs):
super(DummyRequest, self).__init__(*args, **kwargs)
self.GET = {}
self.headers = {}
self.errors = cornice_errors.Errors()
self.authenticated_userid = 'bob'
self.validated = {}
self.matchdict = {}
self.response = mock.MagicMock(headers={})


class PrefixedRequestClass(webtest.app.TestRequest):

@classmethod
Expand Down
Loading