Features
Below is a list of main features that any Resource-based APIs can expose.
Full range of CRUD operations
APIs can support the full range of CRUD operations. The following table shows Resource’s implementation of CRUD via REST:
| Action | HTTP Verb | Context |
|---|---|---|
| Create | POST | List |
| Read | GET | List/Item |
| Update | PATCH | Item |
| Replace | PUT | Item |
| Delete | DELETE | List/Item |
POST
$ curl -i -H "Content-Type: application/json" -d '{"name": "russell", "password": "123456", "date_joined": "datetime(2014-10-11T00:00:00Z)"}' http://127.0.0.1:5000/users
HTTP/1.0 201 CREATED
Content-Type: application/json
Content-Length: 35
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Sat, 11 Oct 2014 13:45:11 GMT
{"_id": "543934671d41c812802711f3"}
GET
Get a list of items.
$ curl -i http://127.0.0.1:5000/users
HTTP/1.0 200 OK
Content-Type: application/json
X-Pagination-Info: page=1, per-page=10, count=1
Content-Length: 117
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Mon, 22 Sep 2014 05:53:01 GMT
[{"password": "123456", "date_joined": "2014-10-11T00:00:00Z", "name": "russell", "_id": "543934671d41c812802711f3"}]
Get a single item.
$ curl -i http://127.0.0.1:5000/users/543934671d41c812802711f3
HTTP/1.0 200 OK
Content-Type: application/json
Content-Length: 115
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Mon, 22 Sep 2014 05:53:01 GMT
{"password": "123456", "date_joined": "2014-10-11T00:00:00Z", "name": "russell", "_id": "543934671d41c812802711f3"}
PATCH
Please refer to RFC 6902 for the exact JSON Patch syntax.
$ curl -i -X PATCH -H "Content-Type: application/json" -d '[{"op": "add", "path": "/password", "value": "666666"}]' http://127.0.0.1:5000/users/543934671d41c812802711f3
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Sat, 11 Oct 2014 13:59:26 GMT
PUT
$ curl -i -X PUT -H "Content-Type: application/json" -d '{"name": "tracey", "password": "123456", "date_joined": "datetime(2014-10-11T00:00:00Z)"}' http://127.0.0.1:5000/users/543934671d41c812802711f3
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Sat, 11 Oct 2014 13:49:00 GMT
DELETE
Delete a list of items.
$ curl -i -X DELETE http://127.0.0.1:5000/users
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Sat, 11 Oct 2014 14:02:00 GMT
Delete a single item.
$ curl -i -X DELETE http://127.0.0.1:5000/users/543934671d41c812802711f3
HTTP/1.0 204 NO CONTENT
Content-Type: application/json
Content-Length: 0
Server: Werkzeug/0.9.6 Python/2.7.3
Date: Sat, 11 Oct 2014 14:02:00 GMT
JSON all the time
All request/response data over the wire is in JSON-format.
Easy and Extensible Data Validation
Please refer to JsonForm for exact validation schema.
from rsrc import Form
class UserForm(Form):
def validate_datetime(value):
if not isinstance(value, datetime):
return 'value must be an instance of `datetime`'
schema = {
'type': 'object',
'properties': {
'name': {'type': 'string'},
'password': {'type': 'string'},
'date_joined': {'custom': validate_datetime}
}
}
Intelligent and Extensible Serializer
Please refer to JsonSir for serialization details.
Deserialize Schema
Convert request data (in JSON/Query-Parameter) to Python type:
| Value in JSON/Query-Parameter | Value in Python |
|---|---|
| "int(10)" | 10 |
| "bool(true)" | True |
| "string" | "string" |
| "regex(/russ/)" | re._pattern_type |
| "objectid(543934671d41c812802711f3)" | bson.ObjectId("543934671d41c812802711f3") |
| "datetime(2014-10-11T00:00:00Z)" | datetime.datetime(2014, 10, 11) |
If you set DATE_FORMAT to "%Y-%m-%d %H:%M:%S", then:
"datetime(2014-10-11 00:00:00)" => datetime.datetime(2014, 10, 11)
Serialize Schema
Convert response data (in Python) to JSON:
| Value in Python | Value in JSON | Value in JSON (WITH_TYPE_NAME == True) |
|---|---|---|
| 10 | 10 | 10 |
| True | true | true |
| "string" | "string" | "string" |
| re._pattern_type | "/russ/" | "regex(/russ/)" |
| bson.ObjectId("543934671d41c812802711f3") | "543934671d41c812802711f3" | "objectid(543934671d41c812802711f3)" |
| datetime.datetime(2014, 10, 11) | "2014-10-11T00:00:00Z" | "datetime(2014-10-11T00:00:00Z)" |
If you set DATE_FORMAT to "%Y-%m-%d %H:%M:%S", then:
datetime.datetime(2014, 10, 11) => "2014-10-11 00:00:00" => "datetime(2014-10-11 00:00:00)"
Filtering
With a single condition:
$ curl http://127.0.0.1:5000/users/543934671d41c812802711f3?name=russell
With multiple conditions in AND mode:
$ curl http://127.0.0.1:5000/users/543934671d41c812802711f3?name=russell&date_joined=datetime(2014-10-11T00:00:00Z)
By subclassing Filter class, you can do more complex filtering:
from rsrc import Filter
class UserFilter(Filter):
def query_date_range(self, params):
date_joined_gt = params.pop('date_joined_gt', None)
date_joined_lt = params.pop('date_joined_lt', None)
conditions = {}
if date_joined_gt:
conditions.update({'$gt': date_joined_gt})
if date_joined_lt:
conditions.update({'$lt': date_joined_lt})
if conditions:
return {'date_joined': conditions}
else:
return {}
Further, with the helper decorator query_params, you can simplify the above UserFilter like this:
from rsrc import Filter, query_params
class UserFilter(Filter):
@query_params('date_joined_gt', 'date_joined_lt')
def query_date_range(self, date_joined_gt=None, date_joined_lt=None):
conditions = {}
if date_joined_gt:
conditions.update({'$gt': date_joined_gt})
if date_joined_lt:
conditions.update({'$lt': date_joined_lt})
if conditions:
return {'date_joined': conditions}
else:
return {}
Then, you can filter users with date_joined_gt and date_joined_lt like this:
$ curl http://127.0.0.1:5000/users?date_joined_gt=datetime(2014-10-01T00:00:00Z)&date_joined_lt=datetime(2014-10-03T00:00:00Z)
DuFilter
Resource is shipped with a class DuFilter, which supports double-underscore style filtering.
With the help of DuFilter, you can filter resources like this (without any extra code):
$ curl http://127.0.0.1:5000/users?username__in=russell&username__in=tracey
$ curl http://127.0.0.1:5000/users?date_joined__gt=datetime(2014-10-01T00:00:00Z)&date_joined__lt=datetime(2014-10-03T00:00:00Z)
Below is the full list of Comparison Operators that DuFilter supports:
| Comparison Operators | Meaning | Example |
|---|---|---|
| ne | not equal | username__ne=russell |
| lt | less than | date_joined__lt=datetime(2014-10-01T00:00:00Z) |
| lte | less than or equal | date_joined__lte=datetime(2014-10-01T00:00:00Z) |
| gt | greater than | date_joined__gt=datetime(2014-10-01T00:00:00Z) |
| gte | greater than or equal | date_joined__gte=datetime(2014-10-01T00:00:00Z) |
| in | within range | username__in=russell&username__in=tracey |
| like | regex match | username__like=^russ |
Pagination
You can paginate items with the following two keywords in query parameter:
| Keyword | Default |
|---|---|
| page | 1 |
| per_page | 20 |
You may want to specify page and per_page explicitly.
$ curl http://127.0.0.1:5000/users?page=2&per_page=10
Sorting
You can sort items with the keyword sort in query parameter:
| Sign | Order |
|---|---|
| + or empty | Ascending |
| - | Descending |
For example, you can sort users first by name in ascending-order and second by age in descending-order:
$ curl http://127.0.0.1:5000/users?sort=name,-age
Fields Selection
You can select/limit returned fields of each item with the keyword fields in query parameter.
For example, the following request will receive all users with only name and password fields in each user:
$ curl http://127.0.0.1:5000/users?fields=name,password
Authentication
Basic Authentication
1. No authentication
For resources protected by Auth, you can access them without credentials:
$ curl http://127.0.0.1:5000/<resource>
2. Simple authentication
from rsrc import BasicAuth
class SimpleAuth(BasicAuth):
def authenticated(self, method, auth_params):
username = auth_params.get('username')
password = auth_params.get('password')
return (username == 'russell' and password == '123456')
For resources protected by SimpleAuth, you must set Authorization header to access them:
$ curl -u russell:encrypted http://127.0.0.1:5000/<resource>
3. Complex authentication
from rsrc import BasicAuth
class ComplexAuth(BasicAuth):
def authenticated(self, method, auth_params):
# allow GET in any case
if method == 'GET':
return True
# lookup in the database
username = auth_params.get('username')
password = auth_params.get('password')
user = db.user.find_one({'username': username, 'password': password})
return bool(user)
For resources protected by ComplexAuth, you can access them via GET without credentials:
$ curl http://127.0.0.1:5000/<resource>
But you must give credentials of a registered user to access these resources via other HTTP methods (i.e. POST, PUT, PATCH, DELETE):
$ curl -u russell:encrypted -X DELETE http://127.0.0.1:5000/<resource>/<pk>
Token-based Authentication
Resource also support Token-based authentication.
1. At server side
First, you must subclass TokenUser:
from bson import ObjectId
from rsrc.contrib.token import TokenUser
class MongoTokenUser(TokenUser):
@classmethod
def get_key(self, username, password):
user = db.user.find_one({'username': username, 'password': password})
if not user:
return None, None
return str(user['_id']), user['jwt_secret']
@classmethod
def exists(self, pk, secret):
if pk is None or secret is None:
return False
user = db.user.find_one({'_id': ObjectId(pk), 'jwt_secret': secret})
return bool(user)
@classmethod
def invalidate_key(cls, pk):
try:
pk = ObjectId(pk)
except:
return False
new_secret = str(uuid.uuid4())
res = db.user.update(
{'_id': pk},
{'$set': {'jwt_secret': new_secret}}
)
return res['updatedExisting']
Next, set class MongoTokenUser (with its module-path) as the value of TOKEN_USER in settings:
TOKEN_USER = '<module_path>.MongoTokenUser'
2. At client side
Then, you can get a token and use it by following the steps below:
-
POST (with username and password) to generate a token
$ curl -X POST -H "Content-Type: application/json" -d '{"username":"russell","password":"123456"}' http://127.0.0.1:5000/tokens -
get the token from the response (in JSON format) of POST
$ {"id": "543934671d41c812802711f3", "token": "eyJhbGciOiJIUzI1NiIsImV4cCI6MTQxNDIyNDExOSwiaWF0IjoxNDE0MjIwNTE5fQ.eyJwayI6IjU0NGI0YWRhMWQ0MWM4MzExMjRhNDBjZCJ9.d_6Oi4ePS7z9NhK9b9J23H3KQx4u_EdzT-VHDnV2fC8", "expires": 3600} -
set the token as username part in the Authorization header to access resources
$ curl -u eyJhbGciOiJIUzI1NiIsImV4cCI6MTQxNDIyNDExOSwiaWF0IjoxNDE0MjIwNTE5fQ.eyJwayI6IjU0NGI0YWRhMWQ0MWM4MzExMjRhNDBjZCJ9.d_6Oi4ePS7z9NhK9b9J23H3KQx4u_EdzT-VHDnV2fC8:unused http://127.0.0.1:5000/<resource>
Sub Resources
Endpoints support sub-resources, in which you can have URI like: /lists/<list_id>/cards.
See demo/mini-trello for example.
Cross Origin
To enable Cross-Origin Resource Sharing (CORS) for your APIs, you should set CROSS_ORIGIN = True in settings. Some CORS control variables are available for you to custom the behaviour.
| CORS variable | Default value |
|---|---|
| ACCESS_CONTROL_ALLOW_ORIGIN | '*' # any domain |
| ACCESS_CONTROL_ALLOW_METHODS | ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] |
| ACCESS_CONTROL_ALLOW_HEADERS | ['Content-Type'] |
| ACCESS_CONTROL_MAX_AGE | 864000 # 10 days |
Support MongoDB
See Demo & Test.
Support RDBMS
Resource supports all RDBMS supported by SQLAlchemy. As of this writing, that includes:
- MySQL (MariaDB)
- PostgreSQL
- SQLite
- Oracle
- Microsoft SQL Server
- Firebird
- Drizzle
- Sybase
- IBM DB2
- SAP Sybase SQL Anywhere
- MonetDB
See Demo & Test.
Support Flask
See Demo & Test.
Support Django
See Demo & Test.