All notes
Ev

RESTful

Representational State Transfer. CNBlog.

Intro

REST and CRUD

The following table shows Eve’s implementation of CRUD via REST:

Action HTTP VerbContext
Create POST Collection
Create PUT Document
Replace PUT Document
Read GET, HEAD Collection/Document
Update PATCH Document
Delete DELETE Collection/Document

Old client not supporting HTTP verbs

As a fallback for the odd client not supporting any of these methods, the API will gladly honor X-HTTP-Method-Override requests. For example a client not supporting the PATCH method could send a POST request with a X-HTTP-Method-Override: PATCH header. The API would then perform a PATCH, overriding the original request method.

payload

curl -i http://eve-demo.herokuapp.com/people

The response payload will look something like this:

{
    "_items": [
        {
            "firstname": "Mark",
            "lastname": "Green",
            "born": "Sat, 23 Feb 1985 12:00:00 GMT",
            "role": ["copy", "author"],
            "location": {"city": "New York", "address": "4925 Lacross Road"},
            "_id": "50bf198338345b1c604faf31",
            "_updated": "Wed, 05 Dec 2012 09:53:07 GMT",
            "_created": "Wed, 05 Dec 2012 09:53:07 GMT",
            "_etag": "ec5e8200b8fa0596afe9ca71a87f23e71ca30e2d",
            "_links": {
                "self": {"href": "people/50bf198338345b1c604faf31", "title": "person"},
            },
        },
        ...
    ],
    "_meta": {
        "max_results": 25,
        "total": 70,
        "page": 1
    },
    "_links": {
        "self": {"href": "people", "title": "people"},
        "parent": {"href": "/", "title": "home"}
    }
}

Sub resources

invoices = {
	'url': 'people/<regex("[a-f0-9]{24}"):contact_id>/invoices'
}
Then this GET to the endpoint, which would roughly translate to give me all the invoices by contact_id:
"people/51f63e0838345b6dcd7eabff/invoices"
Would cause the underlying database collection invoices to be queried this way:
"{'contact_id': '51f63e0838345b6dcd7eabff'}"

Note that when designing your API, most of the time you can get away without resorting to sub-resources, such as by means of filters.

Item endpoints

According to REST principles resource items should only have one unique identifier.

Filters

The mongo query syntax:
http://eve-demo.herokuapp.com/people?where={"lastname": "Doe"}
curl request:
curl -i -g http://eve-demo.herokuapp.com/people?where={%22lastname%22:%20%22Doe%22}
And the native Python syntax:
curl -i http://eve-demo.herokuapp.com/people?where=lastname=="Doe"
Both query formats allow for conditional and logical And/Or operators, however nested and combined.

Filters are enabled by default on all document fields. However, the API maintainer can choose to disable them all and/or whitelist allowed ones (see ALLOWED_FILTERS in Global Configuration). If scraping, or fear of DB DoS attacks by querying on non-indexed fields is a concern, then whitelisting allowed filters is the way to go.

Sorting

curl -i http://eve-demo.herokuapp.com/people?sort=city,-lastname

# The MongoDB data layer also supports native MongoDB syntax:
http://eve-demo.herokuapp.com/people?sort=[("lastname", -1)]
# which translates to the following curl request:
curl -i http://eve-demo.herokuapp.com/people?sort=[(%22lastname%22,%20-1)]
# Note: Always use double quotes to wrap field names and values. Using single quotes will result in 400 Bad Request responses.

Sorting is enabled by default and can be disabled both globally and/or at resource level (see SORTING in Global Configuration and sorting in Domain Configuration). It is also possible to set the default sort at every API endpoints (see default_sort in Domain Configuration).

Pagination

# "max_results" sets the pageSize.
# "page" sets the page number.
http://eve-demo.herokuapp.com/people?max_results=20&page=2

# A mix
http://eve-demo.herokuapp.com/people?where={"lastname": "Doe"}&sort=[("firstname", 1)]&page=5

HATEOAS

Hypermedia as the Engine of Application State (HATEOAS) is enabled by default. Each GET response includes a _links section. Links provide details on their relation relative to the resource being accessed, and a title. Relations and titles can then be used by clients to dynamically updated their UI, or to navigate the API without knowing its structure beforehand.

JSON or XML

# Request for XML instead of JSON.
curl -H "Accept: application/xml" -i http://eve-demo.herokuapp.com

XML support can be disabled by setting XML to False in the settings file. JSON support can be disabled by setting JSON to False. Please note that at least one mime type must always be enabled, either implicitly or explicitly. By default, both are supported.

Conditional Requests

Each resource representation provides information on the last time it was updated (Last-Modified), along with an hash value computed on the representation itself (ETag). These headers allow clients to perform conditional requests by using the If-Modified-Since header:

# Query by time:
curl -H "If-Modified-Since: Wed, 05 Dec 2012 09:53:07 GMT" -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c
# or the If-None-Match header:
curl -H "If-None-Match: 1234567890123456789012345678901234567890" -i http://eve-demo.herokuapp.com/people/521d6840c437dc0002d1203c

eve

Flask routes

StackOverflow. Eve is a Flask application (a subclass actually), so everything you can do with Flask you can do with Eve too (like decorate rendering functions etc.)

from eve import Eve
app = Eve()

@app.route('/hello')
def hello_world():
	return 'Hello World!'

if __name__ == '__main__':
	app.run()

###############

@app.route('/')
def index():
	return app.send_static_file('index.html')

# A better approach will be to prefix the /api for all REST APIs. This can be done by adding URL_PREFIX="api" in settings.py.
# StackOverflow.

app.run(host="0.0.0.0", debug=True)

SSL, https

StackOverflow.

if __name__ == "__main__":
	context = ('cert.crt', 'key.key')
	app.run(host='0.0.0.0', port=80, ssl_context=context, threaded=True, debug=True)

run.py

from eve import Eve
app = Eve()

if __name__ == '__main__':
    app.run()

settings.py

Eve configuration file, a standard Python module.

app = Eve(settings='settings.py')

python run.py
curl -i http://127.0.0.1:5000

curl -u admin:admin 127.0.0.1:50023/accounts?where=username=="test02"

Schema definition

DataTypes

Field data type. Can be one of the following:

	string
	boolean
	integer
	float
	number (integer and float values allowed)
	datetime
	dict
	list
	objectid
	media
# If the MongoDB data layer is used, then geographic data structures are also allowed:
	point
	multipoint
	linestring
	multilinestring
	polygon
	multipolygon
	geometrycollection

Schema keys

type, see before.

Auth

AUTH_FIELD

GithubIssues.
Are you using ID_FIELD as your auth_field? That would cause the issue. Try setting auth_field to something different, like 'user_id'.
You don't need to do anything in the callback function (Eve will save the member['user_id'] value you set set_request_value.
You also should not need to add user_id to the schema (if you do it will be exposed to your clients which is not needed).
If you check the tutorial all that is needed is invoking set_request_auth_value.

Tests

StackOverflow.
Goto eve/tests/auth.py to see how eve programmers write unit tests.
There are two base classes that provide a lot of utility methods: TestMinimal and TestBase. Almost all other test classes inherit from either of those. You probably want to use TestMinimal as it takes care of setting up and dropping the MongoDB connection for you. It also provides stuff like assert200, assert404 etc.
In general, you use the test_client object, as you do with Flask itself.

from eve.tests import TestBase
from eve.tests.test_settings import MONGO_DBNAME

class TestBasicAuth(TestBase):

	def setUp(self):
		super(TestBasicAuth, self).setUp()
		self.app = Eve(settings=self.settings_file, auth=ValidBasicAuth)
		self.test_client = self.app.test_client()
		self.content_type = ('Content-Type', 'application/json')
		self.valid_auth = [('Authorization', 'Basic YWRtaW46c2VjcmV0'),
						   self.content_type]
		self.invalid_auth = [('Authorization', 'Basic IDontThinkSo'),
							 self.content_type]
		self.setUpRoles()
		self.app.set_defaults()

	def setUpRoles(self):
		for _, schema in self.app.config['DOMAIN'].items():
			schema['allowed_roles'] = ['admin']
			schema['allowed_read_roles'] = ['reader']
			schema['allowed_item_roles'] = ['admin']
			schema['allowed_item_read_roles'] = ['reader']
			schema['allowed_item_write_roles'] = ['editor']

	def test_custom_auth(self):
		self.assertTrue(isinstance(self.app.auth, ValidBasicAuth))

    def test_authorized_home_access(self):
        r = self.test_client.get('/', headers=self.valid_auth)
        self.assert200(r.status_code)

    def test_unauthorized_home_access(self):
        r = self.test_client.get('/', headers=self.invalid_auth)
        self.assert401(r.status_code)

	def test_restricted_home_access(self):
		r = self.test_client.get('/')
		self.assert401(r.status_code)

	def test_restricted_resource_access(self):
		r = self.test_client.get(self.known_resource_url)
		self.assert401(r.status_code)
		r = self.test_client.post(self.known_resource_url)
		self.assert401(r.status_code)
		r = self.test_client.delete(self.known_resource_url)
		self.assert401(r.status_code)