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.