diff --git a/docs/build/.doctrees/environment.pickle b/docs/build/.doctrees/environment.pickle index 2459568..e2f15c6 100644 Binary files a/docs/build/.doctrees/environment.pickle and b/docs/build/.doctrees/environment.pickle differ diff --git a/docs/build/.doctrees/index.doctree b/docs/build/.doctrees/index.doctree index 4774476..f8b0da6 100644 Binary files a/docs/build/.doctrees/index.doctree and b/docs/build/.doctrees/index.doctree differ diff --git a/docs/build/.doctrees/introduction.doctree b/docs/build/.doctrees/introduction.doctree index 0b8f096..d0edc5e 100644 Binary files a/docs/build/.doctrees/introduction.doctree and b/docs/build/.doctrees/introduction.doctree differ diff --git a/docs/build/.doctrees/references.doctree b/docs/build/.doctrees/references.doctree index eb204d1..0d236e7 100644 Binary files a/docs/build/.doctrees/references.doctree and b/docs/build/.doctrees/references.doctree differ diff --git a/docs/build/.doctrees/representing_data.doctree b/docs/build/.doctrees/representing_data.doctree index 1ae60d1..838d2ce 100644 Binary files a/docs/build/.doctrees/representing_data.doctree and b/docs/build/.doctrees/representing_data.doctree differ diff --git a/docs/build/.doctrees/tutorial.doctree b/docs/build/.doctrees/tutorial.doctree index 3914f7b..6883200 100644 Binary files a/docs/build/.doctrees/tutorial.doctree and b/docs/build/.doctrees/tutorial.doctree differ diff --git a/docs/build/.doctrees/using_user_endpoint.doctree b/docs/build/.doctrees/using_user_endpoint.doctree index 1d9e928..1db6be0 100644 Binary files a/docs/build/.doctrees/using_user_endpoint.doctree and b/docs/build/.doctrees/using_user_endpoint.doctree differ diff --git a/docs/build/.doctrees/work_with_pagination.doctree b/docs/build/.doctrees/work_with_pagination.doctree new file mode 100644 index 0000000..c62f838 Binary files /dev/null and b/docs/build/.doctrees/work_with_pagination.doctree differ diff --git a/docs/build/_sources/index.txt b/docs/build/_sources/index.txt index d4c2c38..a16039d 100644 --- a/docs/build/_sources/index.txt +++ b/docs/build/_sources/index.txt @@ -3,8 +3,15 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Python Rest Api Framework's documentation! -===================================================== +Python Rest Api Framework's documentation +========================================= + +Python REST API framework is a set of utilities based on werkzeug to +easily build Restful API with a MVC pattern. Main features includes: +Pagination, Authentication, Authorization, Filters, Partials Response, +Error handling, data validators, data formaters... +and more... + Contents: @@ -13,12 +20,12 @@ Contents: introduction tutorial - datastore - controller - pagination - authentication - multiple_endpoint - references +.. datastore +.. controller +.. pagination +.. authentication +.. multiple_endpoint +.. references Indices and tables ================== @@ -27,3 +34,62 @@ Indices and tables * :ref:`modindex` * :ref:`search` + + + +A Full working example +---------------------- + +.. code-block:: python + + from rest_api_framework import models + from rest_api_framework.datastore import SQLiteDataStore + from rest_api_framework.views import JsonResponse + from rest_api_framework.controllers import Controller + from rest_api_framework.datastore.validators import UniqueTogether + from rest_api_framework.pagination import Pagination + + + class UserModel(models.Model): + """ + Define how to handle and validate your data. + """ + fields = [models.StringField(name="first_name", required=True), + models.StringField(name="last_name", required=True), + models.PkField(name="id", required=True) + ] + + + def remove_id(response, obj): + """ + Do not show the id in the response. + """ + obj.pop(response.model.pk_field.name) + return obj + + + class UserEndPoint(Controller): + ressource = { + "ressource_name": "users", + "ressource": {"name": "adress_book.db", "table": "users"}, + "model": UserModel, + "datastore": SQLiteDataStore, + "options": {"validators": [UniqueTogether("first_name", "last_name")]} + } + + controller = { + "list_verbs": ["GET", "POST"], + "unique_verbs": ["GET", "PUT", "DElETE"], + "options": {"pagination": Pagination(20)} + } + + view = {"response_class": JsonResponse, + "options": {"formaters": ["add_ressource_uri", remove_id]}} + + + if __name__ == '__main__': + + from werkzeug.serving import run_simple + from rest_api_framework.controllers import WSGIDispatcher + app = WSGIDispatcher([UserEndPoint]) + run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True) diff --git a/docs/build/_sources/introduction.txt b/docs/build/_sources/introduction.txt index c3f3823..90ce8cb 100644 --- a/docs/build/_sources/introduction.txt +++ b/docs/build/_sources/introduction.txt @@ -346,5 +346,5 @@ is the same as with the PythonListDataStore. Where to go from here --------------------- -* :doc:`Authentication and Authorization ` -* :doc:`multiple_endpoint` +.. * :doc:`Authentication and Authorization ` +.. * :doc:`multiple_endpoint` diff --git a/docs/build/_sources/representing_data.txt b/docs/build/_sources/representing_data.txt index bb030d6..da59b3d 100644 --- a/docs/build/_sources/representing_data.txt +++ b/docs/build/_sources/representing_data.txt @@ -48,7 +48,7 @@ You can check that it work as expected: Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 23:41:55 GMT - {"first_name": "Cap tain", "last_name": "America", + {"first_name": "Captain", "last_name": "America", "ressource_uri": "/users/1/"} Make things generics @@ -77,6 +77,11 @@ Your code then become: obj.pop(response.model.pk_field.name) return obj -And reuse this formatter as long as you need +And reuse this formatter as long as you need. -Next :doc:`related_ressources` +Formaters are here to help you build clean and meaningful ressources +representations. It should hide internal representation of your +ressources and return all of the fields needed to manipulate and +represent your data. + +Next :doc:`work_with_pagination` diff --git a/docs/build/_sources/tutorial.txt b/docs/build/_sources/tutorial.txt index 6abcfa8..ccaefd5 100644 --- a/docs/build/_sources/tutorial.txt +++ b/docs/build/_sources/tutorial.txt @@ -8,4 +8,5 @@ Tutorial building an adressebook API using_user_endpoint adding_validator_datastore representing_data + work_with_pagination related_ressources diff --git a/docs/build/_sources/using_user_endpoint.txt b/docs/build/_sources/using_user_endpoint.txt index 28119fe..5994155 100644 --- a/docs/build/_sources/using_user_endpoint.txt +++ b/docs/build/_sources/using_user_endpoint.txt @@ -9,9 +9,11 @@ First you can check that your endpoint is up HTTP/1.0 200 OK Content-Type: application/json - Content-Length: 2 + Content-Length: 44 Server: Werkzeug/0.8.3 Python/2.7.2 - Date: Mon, 14 Oct 2013 12:52:22 GMT + Date: Tue, 15 Oct 2013 11:13:44 GMT + + {"meta": {"filters": {}}, "object_list": []} Your endpoint is responding but does not have any data. Let's add some: @@ -59,14 +61,14 @@ The list of users is also updated: .. code-block:: bash - curl -i "http://localhost:5000/users/1/" + curl -i "http://localhost:5000/users/" HTTP/1.0 200 OK Content-Type: application/json Content-Length: 83 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 17:03:00 GMT - [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}] + {"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]} Delete a user ------------- @@ -94,8 +96,35 @@ and now delete it: Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 20:41:46 GMT +You can check that the user no longer exists: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/2/" + HTTP/1.0 404 NOT FOUND + Content-Type: application/json + Connection: close + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:16:33 GMT + + {"error": "

The requested URL was not found on the server.

If you entered the URL manually please check your spelling and try again.

"} + +And the list is also updated: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 125 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:17:46 GMT + + {"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]} + + Update a User -============= +------------- Let's go another time to the creation process: @@ -114,24 +143,63 @@ America. Let's update this user: .. code-block:: bash - curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain", "last_name": "America"}' http://localhost:5000/users/3/ + curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Capitain", "last_name": "America"}' http://localhost:5000/users/3/ HTTP/1.0 200 OK Content-Type: application/json Content-Length: 58 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 20:57:47 GMT -Partial update is also possible: + {"first_name": "Capitain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"} + +Argh! Thats a typo. the fist name is "Captain", not "Capitain". Let's +correct this: .. code-block:: bash - curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Cap tain"}' http://localhost:5000/users/3/ + curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain"}' http://localhost:5000/users/3/ HTTP/1.0 200 OK Content-Type: application/json Content-Length: 59 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 21:08:04 GMT + {"first_name": "Captain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"} + + +Filtering +--------- + +Ressources can be filtered easily using parameters: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 236 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:07:21 GMT + + {"meta": {"filters": {"last_name": "America"}}, "object_list": + [{"first_name": "Joe", "last_name": "America", "id": 1, + "ressource_uri": "/users/1/"}, {"first_name": "Bob", "last_name": + "America", "id": 3, "ressource_uri": "/users/3/"}] + +Multiple filters are allowed: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America&first_name=Joe" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 171 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:09:32 GMT + + {"meta": {"filters": {"first_name": "Joe", "last_name": "America"}}, + "object_list": [{"first_name": "Joe", "last_name": "America", "id": 1, + "ressource_uri": "/users/1/"}]} Error handling -------------- diff --git a/docs/build/_sources/work_with_pagination.txt b/docs/build/_sources/work_with_pagination.txt new file mode 100644 index 0000000..eebe89a --- /dev/null +++ b/docs/build/_sources/work_with_pagination.txt @@ -0,0 +1,215 @@ +Working with Pagination +======================= + +Creating fixtures +----------------- + +When your address book will be full of entry, you will need to add a +pagination on your API. As it is a common need, REST API Framework +implement a very easy way of doing so. + +Before you can play with the pagination process, you will need to +create more data. You can create those records the way you want: + +* direct insert into the database + +.. code-block:: bash + + sqlite3 adress_book.db + INSERT INTO users VALUES ("Nick", "Furry", 6); + +* using the datastore directly + +.. code-block:: python + + store = SQLiteDataStore({"name": "adress_book.db", "table": "users"}, UserModel) + store.create({"first_name": "Nick", "last_name": "Furry"}) + +* using your API + +.. code-block:: python + + curl -i -H "Content-type: application/json" -X POST -d '{"first_name": "Nick", "last_name": "Furry"}' http://localhost:5000/users/ + +each on of those methods have advantages and disavantages but they all +make the work done. For this example, I propose to use the well know +requests package with a script to create a bunch of random records: + +For this to work you need to install resquests : http://docs.python-requests.org/en/latest/user/install/#install + +.. code-block:: python + + import json + import requests + import random + import string + + def get_random(): + return ''.join( + random.choice( + string.ascii_letters) for x in range( + int(random.random() * 20) + ) + ) + + for i in range(200): + requests.post("http://localhost:5000/users/", data=json.dumps({"first_name": get_random(), "last_name": get_random()})) + +Pagination +---------- + +Now your datastore is filled with more than 200 records, it's time to +paginate. To do so import Pagination and change the controller part of +your app. + +.. code-block:: python + + from rest_api_framework.pagination import Pagination + + class UserEndPoint(Controller): + ressource = { + "ressource_name": "users", + "ressource": {"name": "adress_book.db", "table": "users"}, + "model": UserModel, + "datastore": SQLiteDataStore, + "options": {"validators": [UniqueTogether("first_name", "last_name")]} + } + + controller = { + "list_verbs": ["GET", "POST"], + "unique_verbs": ["GET", "PUT", "DElETE"], + "options": {"pagination": Pagination(20)} + } + + view = {"response_class": JsonResponse, + "options": {"formaters": ["add_ressource_uri", remove_id]}} + +and try your new pagination: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 1811 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:32:55 GMT + + {"meta": {"count": 20, "total_count": 802, "next": "?offset=20", + "filters": {}, "offset": 0, "previous": "null"}, "object_list": + [{"first_name": "Captain", "last_name": "America", + "ressource_uri": "/users/1/"}, {"first_name": "Captain", + "last_name": "America", "ressource_uri": "/users/3/"}, + {"first_name": "John", "last_name": "Doe", "ressource_uri": + "/users/4/"}, {"first_name": "arRFOSYZT", "last_name": "", + "ressource_uri": "/users/5/"}, {"first_name": "iUJsYORMuYeMUDy", + "last_name": "TqFpmcBQD", "ressource_uri": "/users/6/"}, + {"first_name": "EU", "last_name": "FMSAbcUJBSBDPaF", + "ressource_uri": "/users/7/"}, {"first_name": "mWAwamrMQARXW", + "last_name": "yMNpEnYOPzY", "ressource_uri": "/users/8/"}, + {"first_name": "y", "last_name": "yNiKP", "ressource_uri": + "/users/9/"}, {"first_name": "s", "last_name": "TRT", + "ressource_uri": "/users/10/"}, {"first_name": "", "last_name": + "zFUaBd", "ressource_uri": "/users/11/"}, {"first_name": "WA", + "last_name": "priJ", "ressource_uri": "/users/12/"}, + {"first_name": "XvpLttDqFmR", "last_name": "liU", "ressource_uri": + "/users/13/"}, {"first_name": "ZhJqTgYoEUzmcN", "last_name": + "KKDqHJwJMxPSaTX", "ressource_uri": "/users/14/"}, {"first_name": + "qvUxiKIATdKdkC", "last_name": "wIVzfDlKCkjkHIaC", + "ressource_uri": "/users/15/"}, {"first_name": "YSSMHxdDQQsW", + "last_name": "UaKCKgKsgEe", "ressource_uri": "/users/16/"}, + {"first_name": "EKLFTPJLKDINZio", "last_name": "nuilPTzHqattX", + "ressource_uri": "/users/17/"}, {"first_name": "SPcDBtmDIi", + "last_name": "MrytYqElXiIxA", "ressource_uri": "/users/18/"}, + {"first_name": "OHxNppXiYp", "last_name": "AUvUXFRPICsJIB", + "ressource_uri": "/users/19/"}, {"first_name": "WBFGxnoe", + "last_name": "KG", "ressource_uri": "/users/20/"}, {"first_name": + "i", "last_name": "ggLOcKPpMfgvVGtv", "ressource_uri": + "/users/21/"}]} + +Browsering Through Paginated objects +------------------------------------ + +Of course you get 20 records but the most usefull part is the meta +key: + +.. code-block:: json + + {"meta": + {"count": 20, + "total_count": 802, + "next": "?offset=20", + "filters": {}, + "offset": 0, + "previous": "null"} + } + +You can use the "next" key to retreive the 20 next rows: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?offset=20" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 1849 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:38:59 GMT + +.. code-block:: json + + {"meta": {"count": 20, "total_count": 802, "next": "?offset=40", + "filters": {}, "offset": 20, "previous": "?offset=0"}, "object_list": + []} + +.. note:: + + The count and offset keywords can be easily changed to match your + needs. pagination class may take an offset_key and count_key + parameters. So if you prefer to use first_id and limit, you can + change your Paginator class to do so: + + .. code-block:: python + + "options": {"pagination": Pagination(20, + offset_key="first_id", + count_key="limit") + + Wich will results in the following: + + .. code-block:: bash + + curl -i "http://localhost:5000/users/" + {"meta": {"first_id": 0, "total_count": 802, "next": "?first_id=20", + "limit": 20, "filters": {}, "previous": "null"}, "object_list": [] + + +Pagination and Filters +---------------------- + +Pagination and filtering play nice together + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 298 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:14:59 GMT + + {"meta": {"count": 20, + "total_count": 2, + "next": "null", + "filters": {"last_name": "America"}, + "offset": 0, + "previous": "null"}, + "object_list": [ + {"first_name": "Joe", + "last_name": "America", + "ressource_uri": "/users/1/"}, + {"first_name": "Bob", + "last_name": "America", + "ressource_uri": "/users/3/"} + ] + } diff --git a/docs/build/datastore.html b/docs/build/datastore.html index 8bc2380..40e8ad3 100644 --- a/docs/build/datastore.html +++ b/docs/build/datastore.html @@ -25,7 +25,7 @@ - + @@ -115,8 +115,12 @@

Make things genericsreturn obj -

And reuse this formatter as long as you need

-

Next Linking ressource together

+

And reuse this formatter as long as you need.

+

Formaters are here to help you build clean and meaningful ressources +representations. It should hide internal representation of your +ressources and return all of the fields needed to manipulate and +represent your data.

+

Next Working with Pagination

diff --git a/docs/build/searchindex.js b/docs/build/searchindex.js index 6cf16d2..be2f2e2 100644 --- a/docs/build/searchindex.js +++ b/docs/build/searchindex.js @@ -1 +1 @@ -Search.setIndex({envversion:42,terms:{represent:[3,12,8,9],all:[3,6,7,8,12],code:[6,9],partial:2,list_verb:[3,6,1,4,7],lack:9,steve:2,carfulli:2,follow:[3,9],sould:[12,8],captain:2,row:[12,8],decid:3,typeerror:[],depend:[3,7,8,12],authoris:3,sensit:[],tweet:[3,12,8],show:3,send:[3,12,8],tain:[2,9],decis:3,get_connector:[12,8],islic:[],ressource_list:[],sourc:[12,8],string:8,fals:3,util:3,verb:[3,7,8],mechan:1,objec:[],veri:[3,11,5],hanld:3,retrrreiv:[],list:[3,11,5],last_nam:[2,4,9,6],"try":[2,5,4,9],adress_book:[6,4],uniquetogeth:[],"20th":1,cap:[2,9],naiv:[12,8],rate:[3,7],design:[],pass:8,chek:[12,8],integr:3,compat:12,index:[0,8],what:[],neg:[],abl:[2,1,3,7,8,12],access:[3,5,9],delet:[3,6,1,7],"new":[3,2,4,8,12],method:3,metadata:2,hash:5,behavior:3,gener:[],secondapp:8,here:[],behaviour:[],modif:[],address:[3,10],eventapp:11,becom:9,modifi:3,valu:[3,1,5],search:0,reprensentaion:9,later:9,permit:8,codebas:3,bob:3,explain:2,environn:11,control:[],useful:[3,2],extra:12,appli:2,app:[3,6,8,11],add_ressource_uri:9,from:[],describ:[],fron:[12,8],regist:[],next:[2,4,9,6],everybodi:2,live:3,call:[3,12,8],type:[2,3,4,8,12,9],tell:[3,7],more:[],clever:[],actuali:[3,12,8],under:11,notic:11,"__iter__":[],particular:[3,5],actual:[12,8],must:[2,3,6,5,8,4,12],none:[5,8,12],endpoint:[],alia:[],prepar:[12,8],work:[1,8,5,12,9],uniqu:[2,3,6,7,8,4],firstapp:8,itself:2,obvious:[12,8],whatev:[3,12],learn:3,those:[3,1,12],purpos:5,root:[7,8],def:[5,9],overrid:12,sqlite:[3,12,8],quickstart:[],give:[2,1,3,8,12,9],process:[2,8,6],accept:[],topic:[3,7],want:[2,1,3,6,8,4,12,9],paginated_bi:12,alwai:[1,5,9],cours:2,multipl:[2,10],first_nam:[2,4,9,6],secur:3,anoth:[3,2,5,12,9],write:[11,7,9],how:[],instead:[3,1,9],csv:[3,12],simpl:[],updat:[3,12],map:8,resourc:3,huge:2,max:7,after:[12,8],befor:4,mai:[3,11,12,9],datastor:[],data:[3,5],bind:[],author:[],correspond:3,inform:[3,1,8,12],environ:8,allow:[3,7],callabl:8,talk:11,oper:[12,8],soon:[3,12,8],userendpoint:[6,4,9],paramet:3,render:[3,6,9],fit:3,carri:9,trootl:3,main:[3,8],them:[3,4,8],good:3,"return":[1,3,5,8,12,9],"__getitem__":[],handl:[3,7],auto:[],auth:[5,8],number:[3,12,8],now:[2,5,8,12,9],nor:[],enabl:[3,4,1,8],choic:1,somewher:5,name:[2,3,4,6,5,8,11,12,9],anyth:[3,5,8,12],authent:[],wole:[12,8],easili:[3,5],token:[3,1],each:[2,1,3,4,5,7,8,11,12,9],fulli:[3,5],unique_uri:8,mean:3,compil:3,domain:1,idea:9,unique_verb:[3,6,1,4,7],connect:[5,8,12],our:[3,6,5],meth:[],todo:[],event:11,special:[3,7],out:9,miss:[],newli:[],content:[0,12,4,9,2],rewrit:[],rel:[],sqlitedatastor:3,wich:[3,7,9],to_dict:5,earlier:[],free:[3,12,8],base:[3,1],mime:3,scallabl:3,put:[2,1,3,6,7,4],care:12,reusabl:3,launch:11,could:[2,5,8,12],keep:3,filter:[3,1,8,12],length:[2,4,9],mvc:3,pagin:[3,7],think:12,first:3,feed:3,rang:3,dont:[3,2,9],directli:1,feel:8,onc:[3,12],sqldatastor:12,placehold:5,hook:3,alreadi:3,done:[3,12,8],messag:3,count_kei:[1,8],primari:[12,8],given:5,"long":9,interact:3,least:6,get_list:[12,8],too:[3,5,8,12],statement:[12,8],john:[2,4],"final":12,store:[3,12],schema:[],option:[],wsgiwrapp:8,part:[3,2,9],than:[],serv:[3,6,11],notfound:[5,8,12],keyword:[1,7],provid:[3,2,5,12],remov:[2,1,8,12,9],meen:4,second:[],bee:12,project:6,reus:[3,12,9],ressourc:[],str:[],make_opt:8,fashion:6,apikeyauthent:5,argument:3,have:[2,1,3,4,5,8,10,11,12,9],"__main__":[3,6],need:[2,1,3,6,5,8,10,12,9],check_auth:[5,8],date:[2,4,9],load_url:8,dictionnari:[12,8],self:[5,8,12],client:3,note:[],also:[3,2,12,6],without:[6,8],take:[2,1,3,8,11,12,9],instanci:[1,8,5,12,9],singl:[],even:3,integerfield:[3,12],object:[2,3,5,8,12,9],dialogu:7,plai:[],america:[2,9],"class":[1,3,4,6,5,8,11,12],ressouc:3,don:2,url:[3,7,8],gather:[],flow:3,uri:[2,8],doe:[3,2,4,8,12],ext:1,unique_id:7,clean:3,pattern:3,constrain:[12,8],jsonrespons:[3,6,4,9],apiapp:[3,11],wsgi:8,ressource_uri:[2,9],text:[12,8],syntax:8,fine:5,xml:3,moer:3,onli:[2,1,3,5,7,8,11,12],locat:2,apimodel:[3,12,8],run_simpl:[3,6,11],configur:[3,1],should:[3,6,1,7,5],dict:[3,1,8,5,12],local:3,oct:[2,4,9],variou:4,get:[3,6,1,7,5],stop:[],becaus:[2,1,7,9],get_us:[5,8],cannot:[3,2,12,4,6],other_paramet:[],requir:[3,6,5,8,12],retreiv:[3,2,12,8,6],userapp:11,emb:[],dispatch_request:8,ressoourc:3,yield:[],"public":5,book:[6,10],bad:[2,4],stuff:7,roger:2,common:3,contain:[3,12,8],orign:1,where:[],view:[],respond:2,set:[3,12,8],see:[2,1,4,7,12],mandatori:[],result:[1,3,6,7,8,12],arg:8,close:[12,8],extend:3,databas:[],someth:[2,5,12],unauthoriez:[],behind:9,response_class:[3,6,4,9],between:[3,7,12],"import":[3,6,4,11],insensitv:[],attribut:[1,12,9],accord:[12,8],kei:[5,8,12,9],unauthor:5,itertool:[],extens:3,lazi:[12,8],come:12,famillii:10,protect:5,last:[3,6,12,8],easi:[3,11,12],howev:[2,11],lowercas:[],etc:3,instanc:[3,6,11],grain:5,basestr:[12,8],let:[2,4],com:3,becam:[],"30th":1,can:[2,1,3,4,6,5,8,10,11,12,9],pop:9,loader:8,header:[3,2],dispatch:8,path:[],usermodel:[6,4],wsgi_app:8,backend:[],evalu:[12,8],been:[3,2,12,8],wsgidispatch:[3,6,8,11],json:[3,2,4,9,6],basic:8,validate_field:[12,8],immedi:12,tigh:[],convert:[12,8],ani:[3,2,5],understand:3,togeth:[],required_tru:6,wrapper:[12,8],"case":3,look:[2,3,6,8,11,12],mount:8,properti:3,act:[3,8],formatt:9,defin:[3,1,7],calcul:1,apikei:5,elt:[],abov:1,error:5,"na\u00efv":[],multiple_endpoint:[],pythonlistdatastor:[3,5,8],azerti:5,loos:[12,8],finnali:5,coher:2,advantag:[12,8,9],applic:[2,1,3,6,8,4,9],implment:3,dictonari:3,ratelimit:8,kwarg:[1,8,12],allawi:[],"__init__":[5,8,12],present:[3,8],thei:[3,7,8,5,12],grant:5,perform:5,make:[3,7,8],same:[3,2,4],preserv:9,tutori:[],sqlite3:[3,6,12,8],apiappauth:5,complet:12,http:[2,1,8,9,4],pouet:[],hanl:3,action:[3,5],rais:[2,5,8,12],user:[3,11,5],mani:[3,2,4],outbound:[],respons:[3,2,8,9],implement:[3,7],whole:1,well:[2,12],inherit:[3,1,8,12],pk_field:9,exampl:[],thi:[3,6,1,7,5],fiter:[12,8],apikeyauthor:5,rout:[3,8],usual:3,authmodel:5,construct:8,identifi:[2,3,5,6,7,8,12],just:[2,3,5,8,12,9],dissuss:[],distant:3,expos:3,hint:[],point:[12,8],except:[5,8,12],param:8,exempl:[3,1,4,7],add:[3,2,12,4,11],other:3,els:5,save:[3,6,12,8],adress:6,modul:0,match:[12,8],build:[3,1],real:[2,12,8],indentifi:5,ressource_config:[12,8],format:[3,12],read:[3,7,5],pkfield:[3,6,12,8],mon:[2,4,9],gmt:[2,4,9],password:5,helpful:6,like:[1,3,6,7,11,12],specif:[],integ:[12,8],server:[2,4,9],either:[3,12,8],authorized_method:[],apicontrol:8,twitter:[3,12],creation:[2,12,8],some:[2,1,3,7,8,4,12],back:[12,8],intern:9,use_reload:[3,6,11],peter:2,kawrg:[],insensit:[],definit:[],totali:3,werkzeug:[2,3,6,4,8,11,9],adressebook:[],duplic:2,localhost:[2,4,9],refer:[],machin:3,who:9,run:[3,6],stringfield:[3,6,5,12],autoincr:[12,8],step:[],offset:7,"__name__":[3,6],post:[2,1,3,6,7,8,4],plug:9,about:[3,11,1,12],obj:[12,8,9],memori:[3,12,8],page:[3,0],constructor:11,disabl:3,block:[],subset:[12,8],paginate_bi:[12,8],own:[3,1,8,7,12],"float":[12,8],empti:1,ensur:[2,4,6],chang:[2,3,4,8,12,9],your:[],manag:[3,6,7,8],accordingli:8,wai:[3,5,8,12,9],support:[12,8],use_debugg:[3,6,11],custom:[5,12],start:[3,1,8,12],suit:[3,12],"function":5,properli:[12,8],max_result:8,form:8,tupl:8,link:12,"true":[3,6,5,12,11],count:7,commun:[3,12,8],made:8,possibl:[2,1,8,12,11],"default":9,start_respons:8,displai:3,below:1,limit:[3,1,8,7,12],otherwis:[3,5,8],problem:[2,1],unrel:3,expect:[2,9],datastoreinst:3,featur:7,creat:[],"int":[12,8],request:[2,1,5,7,8,4],"abstract":8,deep:[],repres:[12,8],exist:[3,4,5,8,12],file:[12,8],curl:[2,1,4,9],offset_kei:[1,8],check:[2,3,5,8,4,12,9],herit:8,automaticali:8,when:[3,1,7,9],detail:7,rest_api_framework:[1,3,6,8,11,4,12],field:[],valid:[],test:[3,12,8],remove_id:9,you:[2,1,3,4,5,8,10,11,12,9],ressource_nam:[3,6,7,4],architectur:[],relat:[7,12],finali:3,multimpl:11,developp:11,queri:[12,8],sql:[3,12,8],endoint:11,receiv:[1,8,12],dog:1,descript:[],place:[2,6],time:[2,5,8,12],far:[6,9,11]},objtypes:{"0":"py:module","1":"py:class","2":"py:method"},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"]},filenames:["index","pagination","using_user_endpoint","introduction","adding_validator_datastore","authentication","first_step","controller","references","representing_data","related_ressources","multiple_endpoint","datastore","tutorial"],titles:["Welcome to Python Rest Api Framework’s documentation!","Paginate a ressource","Playing with the newly created endpoint","What is Python REST API Framework","Adding validators to your DataStore","Authentication and Authorization","First Step Building a user endpoint","Controllers","REST API Framework API references","Show data to users","Linking ressource together","Using more than a single endpoint","Datastore","Tutorial building an adressebook API"],objects:{"rest_api_framework.datastore.sql.SQLiteDataStore":{get:[12,2,1,""],get_connector:[12,2,1,""],create:[12,2,1,""],paginate:[12,2,1,""],update:[12,2,1,""],get_list:[12,2,1,""],filter:[12,2,1,""],"delete":[12,2,1,""]},"rest_api_framework.datastore.base.DataStore":{get:[12,2,1,""],create:[12,2,1,""],paginate:[12,2,1,""],update:[12,2,1,""],get_list:[12,2,1,""],filter:[12,2,1,""],validate_fields:[12,2,1,""],validate:[12,2,1,""],"delete":[12,2,1,""]},"rest_api_framework.controllers.ApiController":{index:[8,2,1,""],get:[8,2,1,""],create:[8,2,1,""],paginate:[8,2,1,""],update:[8,2,1,""],get_list:[8,2,1,""],unique_uri:[8,2,1,""],"delete":[8,2,1,""]},"rest_api_framework.datastore.simple.PythonListDataStore":{get:[8,2,1,""],update:[8,2,1,""],get_list:[8,2,1,""]},"rest_api_framework.datastore.sql":{SQLiteDataStore:[12,1,1,""]},"rest_api_framework.datastore.simple":{PythonListDataStore:[8,1,1,""]},"rest_api_framework.controllers":{Controller:[8,1,1,""],WSGIWrapper:[8,1,1,""],ApiController:[8,1,1,""],WSGIDispatcher:[8,1,1,""]},rest_api_framework:{controllers:[8,0,0,"-"]},"rest_api_framework.datastore.base":{DataStore:[12,1,1,""]},"rest_api_framework.controllers.Controller":{load_urls:[8,2,1,""],make_options:[8,2,1,""]},"rest_api_framework.authentication.Authentication":{get_user:[8,2,1,""]},"rest_api_framework.pagination.Pagination":{paginate:[8,2,1,""]},"rest_api_framework.controllers.WSGIWrapper":{dispatch_request:[8,2,1,""],wsgi_app:[8,2,1,""]},"rest_api_framework.pagination":{Pagination:[8,1,1,""]},"rest_api_framework.authentication":{Authentication:[8,1,1,""]}},titleterms:{control:[3,7,8],quickstart:3,creat:[2,12,6],show:9,argument:7,rest:[3,0,8],api:[3,0,8,13],mandatori:7,tabl:0,thing:9,miss:2,your:[12,4],paramet:7,what:3,from:3,describ:12,field:3,databas:3,newli:2,format:9,invalid:2,should:[],avail:12,how:[3,5],make:9,valid:[3,4],summari:6,build:[6,13],indic:0,adressebook:13,sqlitedatastor:8,ressourc:[3,1,12,10],document:0,simpl:[3,8],method:1,singl:11,refer:8,"function":9,model:[3,6,12],option:[3,7],get:2,python:[3,0],max:1,gener:9,first:6,here:3,backend:5,framework:[3,0,8],step:6,base:8,link:10,user:[2,9,6],offset:1,datastor:[3,6,4,8,12],data:[2,9],than:11,welcom:0,count:1,chose:6,endpoint:[2,11,6],handl:2,architectur:3,togeth:10,tutori:13,uniquetogeth:4,author:5,list:2,plai:2,authent:[3,5,8],defin:[6,9],delet:2,exampl:5,pagin:[1,8],updat:2,thi:[],error:2,implement:1,more:11,where:3,other:1,view:[3,6]}}) \ No newline at end of file +Search.setIndex({envversion:42,terms:{represent:[4,12,2,9],all:[4,6,7,2,14,12,9],code:[6,9],partial:0,list_verb:[0,1,4,6,7,8,14],lack:9,steve:3,carfulli:3,follow:[4,14,9],sould:[12,2],captain:[3,14,9],eklftpjlkdinzio:14,row:[12,2,14],decid:4,typeerror:[],depend:[4,7,2,12],authoris:4,sensit:[],tweet:[4,12,2],show:[],readabl:14,send:[4,12,2],tain:[],decis:4,get_connector:[12,2],islic:[],ressource_list:[],sourc:[12,2],string:[2,14],fals:4,util:[4,0],verb:[4,7,2],mechan:1,objec:[],veri:[4,11,5,14],snip:14,mrytyqelxiixa:14,hanld:4,disavantag:14,retrrreiv:[],list:[11,5],last_nam:[0,3,6,8,14,9],"try":[3,5,8,14,9],adress_book:[0,8,14,6],ynikp:14,uniquetogeth:[],"20th":1,pleas:3,cap:[],kkdqhjwjmxpsatx:14,naiv:[12,2],direct:14,famillii:10,second:[],design:[],pass:2,chek:[12,2],iujsyormuyemudi:14,compat:12,index:[0,2],what:[],hide:9,neg:[],abl:[3,1,4,7,2,12],access:[4,5,9],delet:[6,1,7],"new":[3,4,8,14,2,12],ohxnppxiyp:14,method:[],metadata:3,full:14,hash:5,behavior:4,gener:[],secondapp:2,here:[],behaviour:[],modif:[],address:[4,14,10],eventapp:11,becom:9,modifi:4,valu:[4,1,5,14],search:0,auvuxfrpicsjib:14,reprensentaion:9,permit:2,codebas:4,bob:[4,3,14],explain:3,environn:11,control:[],useful:[4,3,14],xvplttdqfmr:14,extra:12,appli:3,app:[0,4,6,2,14,11],prefer:14,add_ressource_uri:[0,14,9],zfuabd:14,instal:14,from:[],describ:[],fron:[12,2],regist:[],next:[3,8,14,9,6],everybodi:3,live:4,call:[4,12,2],typo:3,capitain:3,type:[3,4,8,14,2,12,9],tell:[4,7],more:[],clever:[],actuali:[4,12,2],under:11,notic:11,"__iter__":[],particular:[4,5],actual:[12,2],must:[3,4,6,5,8,2,12],none:[5,2,12],endpoint:[],join:14,alia:[],prepar:[12,2],work:[1,5],uniqu:[3,4,6,7,8,2],firstapp:2,itself:3,obvious:[12,2],whatev:[4,12],learn:4,purpos:5,root:[7,2],def:[0,5,14,9],overrid:12,sqlite:[4,12,2],quickstart:[],give:[3,1,4,2,12,9],process:[3,2,14,6],accept:[],topic:[4,7],want:[3,1,4,6,8,14,2,12,9],paginated_bi:12,alwai:[1,5,9],cours:[3,14],multipl:[3,10],first_nam:[0,3,6,8,14,9],secur:4,anoth:[4,3,5,12,9],object_list:[3,14],ymnpenyopzi:14,write:[11,7,9],how:[],instead:[4,1,9],csv:[4,12],simpl:[],updat:[],map:2,first_id:14,resourc:4,huge:3,max:7,earlier:[],befor:[8,14],mai:[4,11,12,14,9],datastor:[],data:5,bind:[],author:[],correspond:4,inform:[4,1,2,12],environ:2,allow:[4,3,7],enter:3,callabl:2,unique_id:7,oper:[12,2],least:6,help:9,get_random:14,soon:[4,12,2],userendpoint:[0,8,14,9,6],paramet:[],render:[4,6,9],fit:4,gglockppmfgvvgtv:14,carri:9,trootl:4,main:[4,0,2],them:[4,2,8],good:4,"return":[0,1,4,5,2,14,12,9],"__getitem__":[],handl:7,auto:[],spell:3,auth:[5,2],number:[4,12,2],now:[3,5,2,14,12,9],nor:[],enabl:[4,2,1,8],choic:[1,14],somewher:5,name:[0,3,4,6,5,8,14,2,11,12,9],anyth:[4,5,2,12],total_count:14,authent:[],wole:[12,2],easili:[4,0,5,14,3],token:[4,1],each:[3,1,4,5,7,8,14,2,11,12,9],fulli:[4,5],unique_uri:2,mean:4,compil:4,nick:14,domain:1,idea:9,meta:[3,14],unique_verb:[0,1,4,6,7,8,14],connect:[3,5,2,12],fist:3,our:[4,6,5],meth:[],todo:[],event:11,special:[4,7],out:9,miss:[],newli:[],content:[0,3,8,14,12,9],rewrit:[],uakckgksge:14,rel:[],sqlitedatastor:[],wich:[4,7,14,9],correct:3,to_dict:5,after:[12,2],found:3,manipul:9,free:[4,12,2],argh:3,base:1,mime:4,scallabl:4,latest:14,put:[0,1,3,4,6,7,8,14],org:14,care:12,resquest:14,reusabl:4,launch:11,could:[3,5,2,12],keep:4,filter:[1,2,12],length:[3,8,14,9],mvc:[4,0],pagin:7,think:12,first:[],feed:4,rang:[4,14],dont:[4,3,9],directli:[1,14],feel:2,onc:[4,12],sqldatastor:12,placehold:5,hook:4,alreadi:4,done:[4,12,2,14],messag:4,count_kei:[1,2,14],primari:[12,2],given:5,"long":9,script:14,interact:4,wrapper:[12,2],get_list:[12,2],too:[4,5,2,12],statement:[12,2],john:[3,8,14],"final":12,store:[4,12,14],schema:[],option:[],nuilptzhqattx:14,wsgiwrapp:2,part:[4,3,14,9],than:[],ascii_lett:14,serv:[4,0,11,6],notfound:[5,2,12],keyword:[1,7,14],provid:[4,3,5,12],remov:[3,1,2,12,9],meen:8,rate:[4,7],bee:12,project:6,reus:[4,12,9],ressourc:[],str:[],entri:14,make_opt:2,fashion:6,apikeyauthent:5,argument:[],packag:14,have:[3,1,2,4,5,8,14,10,11,12,9],"__main__":[4,0,6],need:[3,1,4,6,5,2,14,10,12,9],"null":14,check_auth:[5,2],date:[3,8,14,9],load_url:2,dictionnari:[12,2],self:[5,2,12],client:4,note:[],also:[4,3,12,6],without:[6,2],take:[3,1,4,2,14,11,12,9],instanci:[1,2,5,12,9],singl:[],even:4,integerfield:[4,12],previou:14,dialogu:7,most:14,plai:[],america:[3,14,9],"class":[0,1,4,6,5,8,14,2,11,12],ressouc:4,don:3,url:[4,3,7,2],doc:14,later:9,flow:4,uri:[3,2],doe:[3,4,8,14,2,12],ext:1,talk:11,clean:[4,9],pattern:[4,0],constrain:[12,2],propos:14,jsonrespons:[0,4,6,8,14,9],apiapp:[4,11],wsgi:2,prij:14,ressource_uri:[3,14,9],text:[12,2],random:14,syntax:2,fine:5,xml:4,moer:4,onli:[3,1,4,5,7,2,11,12],locat:3,apimodel:[4,12,2],run_simpl:[4,0,11,6],configur:[4,1],should:[6,1,7,5],dict:[4,1,2,5,12],local:4,oct:[3,8,14,9],variou:8,get:[6,1,7,5],stop:[],becaus:[3,1,7,9],get_us:[5,2],cannot:[4,3,12,8,6],other_paramet:[],requir:[0,4,6,5,2,12],liu:14,retreiv:[3,4,6,2,14,12],userapp:11,emb:[],dispatch_request:2,ressoourc:4,yield:[],"public":5,book:[6,14,10],bad:[3,8],stuff:7,roger:3,common:[4,14],contain:[4,12,2],orign:1,where:[],view:[],respond:3,set:[4,0,12,2],dump:14,see:[3,1,8,7,12],mandatori:[],result:[1,4,6,7,2,14,12],arg:2,close:[3,12,2],wbfgxnoe:14,extend:4,databas:[],someth:[3,5,12],unauthoriez:[],behind:9,response_class:[0,4,6,8,14,9],between:[4,7,12],"import":[0,4,6,8,14,11],bunch:14,qvuxikiatdkdkc:14,insensitv:[],attribut:[1,12,9],accord:[12,2],kei:[5,2,12,9,14],unauthor:5,itertool:[],extens:4,lazi:[12,2],joe:[3,14],come:12,tue:[3,14],protect:5,last:[4,6,12,2],easi:[4,11,12,14],howev:[3,11],lowercas:[],etc:4,instanc:[4,6,11],grain:5,basestr:[12,2],let:[3,8],com:4,becam:[],"30th":1,mwawamrmqarxw:14,can:[3,1,2,4,6,5,8,14,10,11,12,9],pop:[0,9],loader:2,header:[4,3],dispatch:2,path:[],usermodel:[0,8,14,6],wsgi_app:2,duplic:3,evalu:[12,2],been:[4,3,12,2],wsgidispatch:[4,0,11,2,6],json:[3,4,6,8,14,9],arrfosyzt:14,basic:2,validate_field:[12,2],immedi:12,tigh:[],convert:[12,2],ani:[4,3,5],understand:4,togeth:[],furri:14,required_tru:6,those:[4,1,12,14],"case":4,look:[3,4,6,2,11,12],mount:2,properti:4,act:[4,2],formatt:9,defin:[1,7],calcul:1,apikei:5,elt:[],abov:1,error:5,"na\u00efv":[],multiple_endpoint:[],pythonlistdatastor:[4,5,2],azerti:5,loos:[12,2],finnali:5,coher:3,advantag:[12,2,14,9],applic:[3,1,4,6,8,14,2,9],implment:4,dictonari:4,ratelimit:2,kwarg:[1,2,12],allawi:[],"__init__":[5,2,12],present:[4,2],thei:[4,5,7,2,14,12],grant:5,perform:5,make:7,same:[4,3,8],preserv:9,tutori:[],sqlite3:[4,6,12,2,14],apiappauth:5,complet:12,http:[3,1,8,14,2,9],pouet:[],hanl:4,wivzfdlkckjkhiac:14,action:[4,5],rais:[3,5,2,12],user:[11,5],mani:[4,3,8],outbound:[],respons:[4,0,2,9,3],implement:7,whole:1,well:[3,12,14],inherit:[4,1,2,12],pk_field:[0,9],exampl:[],thi:[6,1,7,5],fiter:[12,2],yssmhxddqqsw:14,apikeyauthor:5,rout:[4,2],usual:4,authmodel:5,construct:2,identifi:[3,4,5,6,7,2,12],just:[3,4,5,2,12,9],object:[5,9],dissuss:[],distant:4,fmsabcujbsbdpaf:14,gather:[],expos:4,hint:[],point:[12,2],except:[5,2,12],param:2,exempl:[4,1,8,7],add:[3,4,8,14,11,12],other:[],els:5,save:[4,6,12,2],adress:6,modul:0,match:[12,2,14],build:1,real:[3,12,2],indentifi:5,ressource_config:[12,2],format:[],read:[4,7,5],pkfield:[4,0,12,2,6],mon:[3,8,9],gmt:[3,8,14,9],password:5,helpful:6,insert:14,like:[1,4,6,7,11,12],specif:[],manual:3,integ:[12,2],server:[3,8,14,9],either:[4,12,2],authorized_method:[],architectur:[],apicontrol:2,twitter:[4,12],creation:[3,12,2],some:[3,1,4,7,8,2,12],back:[12,2],intern:9,use_reload:[4,0,11,6],peter:3,kawrg:[],insensit:[],definit:[],totali:4,werkzeug:[0,3,4,6,8,14,2,11,9],adressebook:[],backend:[],localhost:[3,8,14,9],refer:[],machin:4,who:9,run:[4,6],spcdbtmdii:14,stringfield:[4,0,5,12,6],autoincr:[12,2],step:[],offset:7,"__name__":[4,0,6],post:[0,1,3,4,6,7,8,14,2],plug:9,about:[4,11,1,12],obj:[0,12,2,9],memori:[4,12,2],page:[4,0],integr:4,constructor:11,disabl:4,block:[],subset:[12,2],paginate_bi:[12,2],own:[4,1,2,7,12],"float":[12,2],empti:1,ensur:[3,8,6],chang:[3,4,8,14,2,12,9],your:[],manag:[4,6,7,2],accordingli:2,wai:[4,5,2,14,12,9],support:[12,2],use_debugg:[4,0,11,6],custom:[5,12],start:[4,1,2,12],includ:0,suit:[4,12],"function":5,properli:[12,2],max_result:2,form:2,tupl:2,link:[],"true":[0,4,6,5,11,12],count:7,commun:[4,12,2],made:2,possibl:[11,1,2,12],"default":9,start_respons:2,displai:4,trt:14,record:14,below:1,limit:[1,4,7,2,14,12],otherwis:[4,5,2],problem:[3,1],unrel:4,expect:[3,9],datastoreinst:4,featur:[0,7],creat:[],"int":[12,2,14],tqfpmcbqd:14,"abstract":2,deep:[],repres:[12,2,9],exist:[3,4,5,8,2,12],file:[12,2],request:[3,1,5,7,8,14,2],curl:[3,1,8,14,9],offset_kei:[1,2,14],check:[3,4,5,8,2,12,9],fill:14,again:3,herit:2,know:14,automaticali:2,when:[4,1,7,9,14],detail:7,rest_api_framework:[0,1,4,6,8,14,2,11,12],field:[],valid:[],test:[4,12,2],remove_id:[0,14,9],you:[3,1,2,4,5,8,14,10,11,12,9],ressource_nam:[0,4,6,7,8,14],nice:14,relat:[7,12],finali:4,zhjqtgyoeuzmcn:14,multimpl:11,developp:11,meaning:9,queri:[12,2],sql:[4,12,2],endoint:11,receiv:[1,2,12],longer:3,dog:1,descript:[],place:[3,6],time:[3,5,2,12,14],far:[6,9,11]},objtypes:{"0":"py:module","1":"py:class","2":"py:method"},objnames:{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","method","Python method"]},filenames:["index","pagination","references","using_user_endpoint","introduction","authentication","first_step","controller","adding_validator_datastore","representing_data","related_ressources","multiple_endpoint","datastore","tutorial","work_with_pagination"],titles:["Python Rest Api Framework’s documentation","Paginate a ressource","REST API Framework API references","Playing with the newly created endpoint","What is Python REST API Framework","Authentication and Authorization","First Step Building a user endpoint","Controllers","Adding validators to your DataStore","Show data to users","Linking ressource together","Using more than a single endpoint","Datastore","Tutorial building an adressebook API","Working with Pagination"],objects:{"rest_api_framework.datastore.sql.SQLiteDataStore":{get:[12,2,1,""],get_connector:[12,2,1,""],create:[12,2,1,""],paginate:[12,2,1,""],update:[12,2,1,""],get_list:[12,2,1,""],filter:[12,2,1,""],"delete":[12,2,1,""]},"rest_api_framework.datastore.base.DataStore":{get:[12,2,1,""],create:[12,2,1,""],paginate:[12,2,1,""],update:[12,2,1,""],get_list:[12,2,1,""],filter:[12,2,1,""],validate_fields:[12,2,1,""],validate:[12,2,1,""],"delete":[12,2,1,""]},"rest_api_framework.controllers.ApiController":{index:[2,2,1,""],get:[2,2,1,""],create:[2,2,1,""],paginate:[2,2,1,""],update:[2,2,1,""],get_list:[2,2,1,""],unique_uri:[2,2,1,""],"delete":[2,2,1,""]},"rest_api_framework.datastore.simple.PythonListDataStore":{get:[2,2,1,""],update:[2,2,1,""],get_list:[2,2,1,""]},"rest_api_framework.datastore.sql":{SQLiteDataStore:[12,1,1,""]},"rest_api_framework.datastore.simple":{PythonListDataStore:[2,1,1,""]},"rest_api_framework.controllers":{Controller:[2,1,1,""],WSGIWrapper:[2,1,1,""],ApiController:[2,1,1,""],WSGIDispatcher:[2,1,1,""]},rest_api_framework:{controllers:[2,0,0,"-"]},"rest_api_framework.datastore.base":{DataStore:[12,1,1,""]},"rest_api_framework.controllers.Controller":{load_urls:[2,2,1,""],make_options:[2,2,1,""]},"rest_api_framework.authentication.Authentication":{get_user:[2,2,1,""]},"rest_api_framework.pagination.Pagination":{paginate:[2,2,1,""]},"rest_api_framework.controllers.WSGIWrapper":{dispatch_request:[2,2,1,""],wsgi_app:[2,2,1,""]},"rest_api_framework.pagination":{Pagination:[2,1,1,""]},"rest_api_framework.authentication":{Authentication:[2,1,1,""]}},titleterms:{show:9,rest:[4,0,2],through:14,paramet:7,how:[4,5],should:[],other:1,format:9,python:[4,0],framework:[4,0,2],list:3,authent:[4,5,2],updat:3,where:4,summari:6,mandatori:7,what:4,databas:4,newli:3,delet:3,adressebook:13,sqlitedatastor:2,method:1,refer:2,full:0,chose:6,gener:9,here:4,step:6,base:2,offset:1,filter:[3,14],thing:9,tutori:13,pagin:[1,2,14],implement:1,view:[4,6],first:6,api:[4,0,2,13],miss:3,your:[12,8],backend:5,from:4,describ:12,avail:12,handl:3,more:11,"function":9,option:[4,7],link:10,togeth:10,than:11,count:1,endpoint:[3,11,6],uniquetogeth:8,work:[0,14],defin:[6,9],ressourc:[4,1,12,10],error:3,fixtur:14,browser:14,control:[4,7,2],quickstart:4,creat:[3,12,14,6],argument:7,indic:0,tabl:0,welcom:[],author:5,make:9,get:3,invalid:3,field:4,valid:[4,8],build:[6,13],document:0,simpl:[4,2],singl:11,architectur:4,max:1,object:14,plai:3,user:[3,9,6],datastor:[4,6,12,8,2],data:[3,9],exampl:[0,5],thi:[],model:[4,6,12]}}) \ No newline at end of file diff --git a/docs/build/tutorial.html b/docs/build/tutorial.html index 66b577a..6838f03 100644 --- a/docs/build/tutorial.html +++ b/docs/build/tutorial.html @@ -68,9 +68,8 @@

Tutorial building an adressebook APICreate a user
  • List and Get
  • Delete a user
  • - - -
  • Update a User
  • @@ -83,6 +82,13 @@

    Tutorial building an adressebook APIMake things generics +
  • Working with Pagination +
  • Linking ressource together
  • diff --git a/docs/build/using_user_endpoint.html b/docs/build/using_user_endpoint.html index 05e6be6..9d09ec5 100644 --- a/docs/build/using_user_endpoint.html +++ b/docs/build/using_user_endpoint.html @@ -61,9 +61,11 @@

    Playing with the newly created endpoint{"meta": {"filters": {}}, "object_list": []}

    Your endpoint is responding but does not have any data. Let’s add @@ -102,14 +104,14 @@

    List and GetShow data to users

    The list of users is also updated:

    -
    curl -i "http://localhost:5000/users/1/"
    +
    curl -i "http://localhost:5000/users/"
     HTTP/1.0 200 OK
     Content-Type: application/json
     Content-Length: 83
     Server: Werkzeug/0.8.3 Python/2.7.2
     Date: Mon, 14 Oct 2013 17:03:00 GMT
     
    -[{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]
    +{"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]}
     
    @@ -135,10 +137,31 @@

    Delete a user
    curl -i "http://localhost:5000/users/2/"
    +HTTP/1.0 404 NOT FOUND
    +Content-Type: application/json
    +Connection: close
    +Server: Werkzeug/0.8.3 Python/2.7.2
    +Date: Tue, 15 Oct 2013 11:16:33 GMT
    +
    +{"error": "<p>The requested URL was not found on the server.</p><p>If you entered the URL manually please check your spelling and try again.</p>"}
    +
    +

    +

    And the list is also updated:

    +
    curl -i "http://localhost:5000/users/"
    +HTTP/1.0 200 OK
    +Content-Type: application/json
    +Content-Length: 125
    +Server: Werkzeug/0.8.3 Python/2.7.2
    +Date: Tue, 15 Oct 2013 11:17:46 GMT
    +
    +{"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]}
    +
    -

    Update a User

    +

    Update a User

    Let’s go another time to the creation process:

    curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"Steve", "last_name": "Roger"}'  http://localhost:5000/users/
     HTTP/1.0 201 CREATED
    @@ -151,23 +174,59 @@ 

    Update a User
    curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain", "last_name": "America"}'  http://localhost:5000/users/3/
    +
    curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Capitain", "last_name": "America"}'  http://localhost:5000/users/3/
     HTTP/1.0 200 OK
     Content-Type: application/json
     Content-Length: 58
     Server: Werkzeug/0.8.3 Python/2.7.2
     Date: Mon, 14 Oct 2013 20:57:47 GMT
    +
    +{"first_name": "Capitain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"}
     
    -

    Partial update is also possible:

    -
    curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Cap tain"}'  http://localhost:5000/users/3/
    +

    Argh! Thats a typo. the fist name is “Captain”, not “Capitain”. Let’s +correct this:

    +
    curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain"}'  http://localhost:5000/users/3/
     HTTP/1.0 200 OK
     Content-Type: application/json
     Content-Length: 59
     Server: Werkzeug/0.8.3 Python/2.7.2
     Date: Mon, 14 Oct 2013 21:08:04 GMT
    +
    +{"first_name": "Captain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"}
     
    +
    +
    +

    Filtering

    +

    Ressources can be filtered easily using parameters:

    +
    curl -i "http://localhost:5000/users/?last_name=America"
    +HTTP/1.0 200 OK
    +Content-Type: application/json
    +Content-Length: 236
    +Server: Werkzeug/0.8.3 Python/2.7.2
    +Date: Tue, 15 Oct 2013 12:07:21 GMT
    +
    +{"meta": {"filters": {"last_name": "America"}}, "object_list":
    +[{"first_name": "Joe", "last_name": "America", "id": 1,
    +"ressource_uri": "/users/1/"}, {"first_name": "Bob", "last_name":
    +"America", "id": 3, "ressource_uri": "/users/3/"}]
    +
    +
    +

    Multiple filters are allowed:

    +
    curl -i "http://localhost:5000/users/?last_name=America&first_name=Joe"
    +HTTP/1.0 200 OK
    +Content-Type: application/json
    +Content-Length: 171
    +Server: Werkzeug/0.8.3 Python/2.7.2
    +Date: Tue, 15 Oct 2013 12:09:32 GMT
    +
    +{"meta": {"filters": {"first_name": "Joe", "last_name": "America"}},
    +"object_list": [{"first_name": "Joe", "last_name": "America", "id": 1,
    +"ressource_uri": "/users/1/"}]}
    +
    +
    +

    Error handling

    Of course, If data is not formated as expected by the API, the base @@ -224,9 +283,8 @@

    Table Of Contents

  • Create a user
  • List and Get
  • Delete a user
  • - - -
  • Update a User
      +
    • Update a User
    • +
    • Filtering
    • Error handling
      • Missing data
      • Invalid Data
      • diff --git a/docs/build/work_with_pagination.html b/docs/build/work_with_pagination.html new file mode 100644 index 0000000..cb199ef --- /dev/null +++ b/docs/build/work_with_pagination.html @@ -0,0 +1,299 @@ + + + + + + + + Working with Pagination — Python Rest Api Framework 0.1 documentation + + + + + + + + + + + + + +
        +
        +
        +
        + +
        +

        Working with Pagination

        +
        +

        Creating fixtures

        +

        When your address book will be full of entry, you will need to add a +pagination on your API. As it is a common need, REST API Framework +implement a very easy way of doing so.

        +

        Before you can play with the pagination process, you will need to +create more data. You can create those records the way you want:

        +
          +
        • direct insert into the database
        • +
        +
        sqlite3 adress_book.db
        +INSERT INTO users VALUES ("Nick", "Furry", 6);
        +
        +
        +
          +
        • using the datastore directly
        • +
        +
        store = SQLiteDataStore({"name": "adress_book.db", "table": "users"}, UserModel)
        +store.create({"first_name": "Nick", "last_name": "Furry"})
        +
        +
        +
          +
        • using your API
        • +
        +
        curl -i -H "Content-type: application/json" -X POST -d '{"first_name": "Nick", "last_name": "Furry"}'  http://localhost:5000/users/
        +
        +
        +

        each on of those methods have advantages and disavantages but they all +make the work done. For this example, I propose to use the well know +requests package with a script to create a bunch of random records:

        +

        For this to work you need to install resquests : http://docs.python-requests.org/en/latest/user/install/#install

        +
        import json
        +import requests
        +import random
        +import string
        +
        +def get_random():
        +    return ''.join(
        +                   random.choice(
        +                     string.ascii_letters) for x in range(
        +                     int(random.random() * 20)
        +                     )
        +                   )
        +
        +for i in range(200):
        +    requests.post("http://localhost:5000/users/", data=json.dumps({"first_name": get_random(), "last_name": get_random()}))
        +
        +
        +
        + +
        +

        Browsering Through Paginated objects

        +

        Of course you get 20 records but the most usefull part is the meta +key:

        +
        {"meta":
        +    {"count": 20,
        +    "total_count": 802,
        +    "next": "?offset=20",
        +    "filters": {},
        +    "offset": 0,
        +    "previous": "null"}
        +}
        +
        +
        +

        You can use the “next” key to retreive the 20 next rows:

        +
        curl -i "http://localhost:5000/users/?offset=20"
        +HTTP/1.0 200 OK
        +Content-Type: application/json
        +Content-Length: 1849
        +Server: Werkzeug/0.8.3 Python/2.7.2
        +Date: Tue, 15 Oct 2013 11:38:59 GMT
        +
        +
        +
        {"meta": {"count": 20, "total_count": 802, "next": "?offset=40",
        +"filters": {}, "offset": 20, "previous": "?offset=0"}, "object_list":
        +[<snip for readability>]}
        +
        +
        +

        Note

        +

        The count and offset keywords can be easily changed to match your +needs. pagination class may take an offset_key and count_key +parameters. So if you prefer to use first_id and limit, you can +change your Paginator class to do so:

        +
        "options": {"pagination": Pagination(20,
        +                                 offset_key="first_id",
        +                                 count_key="limit")
        +
        +
        +

        Wich will results in the following:

        +
        curl -i "http://localhost:5000/users/"
        +{"meta": {"first_id": 0, "total_count": 802, "next": "?first_id=20",
        +"limit": 20, "filters": {}, "previous": "null"}, "object_list": [<snip
        +for readability>]
        +
        +
        +
        +
        +
        +

        Pagination and Filters

        +

        Pagination and filtering play nice together

        +
        curl -i "http://localhost:5000/users/?last_name=America"
        +HTTP/1.0 200 OK
        +Content-Type: application/json
        +Content-Length: 298
        +Server: Werkzeug/0.8.3 Python/2.7.2
        +Date: Tue, 15 Oct 2013 12:14:59 GMT
        +
        +{"meta": {"count": 20,
        +          "total_count": 2,
        +          "next": "null",
        +          "filters": {"last_name": "America"},
        +          "offset": 0,
        +          "previous": "null"},
        +          "object_list": [
        +              {"first_name": "Joe",
        +               "last_name": "America",
        +               "ressource_uri": "/users/1/"},
        +              {"first_name": "Bob",
        +               "last_name": "America",
        +               "ressource_uri": "/users/3/"}
        +          ]
        + }
        +
        +
        +
        +
        + + +
        +
        +
        +
        +
        +

        Table Of Contents

        + + +

        This Page

        + + + +
        +
        +
        +
        + + + + \ No newline at end of file diff --git a/docs/source/#related_ressources.rst# b/docs/source/#related_ressources.rst# new file mode 100644 index 0000000..84fc2b9 --- /dev/null +++ b/docs/source/#related_ressources.rst# @@ -0,0 +1,7 @@ +Linking ressource together +========================== + + + +Address Book need to link address to user as you can have multiple +users with a single address (a familliy for example) diff --git a/docs/source/.#related_ressources.rst b/docs/source/.#related_ressources.rst new file mode 120000 index 0000000..67b9448 --- /dev/null +++ b/docs/source/.#related_ressources.rst @@ -0,0 +1 @@ +yohann@MacBook-Air-de-Yohann.local.36008 \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index d4c2c38..d43f9fa 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -3,8 +3,15 @@ You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to Python Rest Api Framework's documentation! -===================================================== +Python Rest Api Framework's documentation +========================================= + +Python REST API framework is a set of utilities based on werkzeug to +easily build Restful API with a MVC pattern. Main features includes: +Pagination, Authentication, Authorization, Filters, Partials Response, +Error handling, data validators, data formaters... +and more... + Contents: @@ -13,12 +20,70 @@ Contents: introduction tutorial - datastore - controller - pagination - authentication - multiple_endpoint - references +.. datastore +.. controller +.. pagination +.. authentication +.. multiple_endpoint +.. references + + +A Full working example +---------------------- + +.. code-block:: python + + from rest_api_framework import models + from rest_api_framework.datastore import SQLiteDataStore + from rest_api_framework.views import JsonResponse + from rest_api_framework.controllers import Controller + from rest_api_framework.datastore.validators import UniqueTogether + from rest_api_framework.pagination import Pagination + + + class UserModel(models.Model): + """ + Define how to handle and validate your data. + """ + fields = [models.StringField(name="first_name", required=True), + models.StringField(name="last_name", required=True), + models.PkField(name="id", required=True) + ] + + + def remove_id(response, obj): + """ + Do not show the id in the response. + """ + obj.pop(response.model.pk_field.name) + return obj + + + class UserEndPoint(Controller): + ressource = { + "ressource_name": "users", + "ressource": {"name": "adress_book.db", "table": "users"}, + "model": UserModel, + "datastore": SQLiteDataStore, + "options": {"validators": [UniqueTogether("first_name", "last_name")]} + } + + controller = { + "list_verbs": ["GET", "POST"], + "unique_verbs": ["GET", "PUT", "DElETE"], + "options": {"pagination": Pagination(20)} + } + + view = {"response_class": JsonResponse, + "options": {"formaters": ["add_ressource_uri", remove_id]}} + + + if __name__ == '__main__': + + from werkzeug.serving import run_simple + from rest_api_framework.controllers import WSGIDispatcher + app = WSGIDispatcher([UserEndPoint]) + run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True) Indices and tables ================== @@ -26,4 +91,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - diff --git a/docs/source/introduction.rst b/docs/source/introduction.rst index c3f3823..90ce8cb 100644 --- a/docs/source/introduction.rst +++ b/docs/source/introduction.rst @@ -346,5 +346,5 @@ is the same as with the PythonListDataStore. Where to go from here --------------------- -* :doc:`Authentication and Authorization ` -* :doc:`multiple_endpoint` +.. * :doc:`Authentication and Authorization ` +.. * :doc:`multiple_endpoint` diff --git a/docs/source/representing_data.rst b/docs/source/representing_data.rst index bb030d6..da59b3d 100644 --- a/docs/source/representing_data.rst +++ b/docs/source/representing_data.rst @@ -48,7 +48,7 @@ You can check that it work as expected: Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 23:41:55 GMT - {"first_name": "Cap tain", "last_name": "America", + {"first_name": "Captain", "last_name": "America", "ressource_uri": "/users/1/"} Make things generics @@ -77,6 +77,11 @@ Your code then become: obj.pop(response.model.pk_field.name) return obj -And reuse this formatter as long as you need +And reuse this formatter as long as you need. -Next :doc:`related_ressources` +Formaters are here to help you build clean and meaningful ressources +representations. It should hide internal representation of your +ressources and return all of the fields needed to manipulate and +represent your data. + +Next :doc:`work_with_pagination` diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst index 6abcfa8..ccaefd5 100644 --- a/docs/source/tutorial.rst +++ b/docs/source/tutorial.rst @@ -8,4 +8,5 @@ Tutorial building an adressebook API using_user_endpoint adding_validator_datastore representing_data + work_with_pagination related_ressources diff --git a/docs/source/using_user_endpoint.rst b/docs/source/using_user_endpoint.rst index 28119fe..5994155 100644 --- a/docs/source/using_user_endpoint.rst +++ b/docs/source/using_user_endpoint.rst @@ -9,9 +9,11 @@ First you can check that your endpoint is up HTTP/1.0 200 OK Content-Type: application/json - Content-Length: 2 + Content-Length: 44 Server: Werkzeug/0.8.3 Python/2.7.2 - Date: Mon, 14 Oct 2013 12:52:22 GMT + Date: Tue, 15 Oct 2013 11:13:44 GMT + + {"meta": {"filters": {}}, "object_list": []} Your endpoint is responding but does not have any data. Let's add some: @@ -59,14 +61,14 @@ The list of users is also updated: .. code-block:: bash - curl -i "http://localhost:5000/users/1/" + curl -i "http://localhost:5000/users/" HTTP/1.0 200 OK Content-Type: application/json Content-Length: 83 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 17:03:00 GMT - [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}] + {"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]} Delete a user ------------- @@ -94,8 +96,35 @@ and now delete it: Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 20:41:46 GMT +You can check that the user no longer exists: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/2/" + HTTP/1.0 404 NOT FOUND + Content-Type: application/json + Connection: close + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:16:33 GMT + + {"error": "

        The requested URL was not found on the server.

        If you entered the URL manually please check your spelling and try again.

        "} + +And the list is also updated: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 125 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:17:46 GMT + + {"meta": {"filters": {}}, "object_list": [{"first_name": "John", "last_name": "Doe", "id": 1, "ressource_uri": "/users/1/"}]} + + Update a User -============= +------------- Let's go another time to the creation process: @@ -114,24 +143,63 @@ America. Let's update this user: .. code-block:: bash - curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain", "last_name": "America"}' http://localhost:5000/users/3/ + curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Capitain", "last_name": "America"}' http://localhost:5000/users/3/ HTTP/1.0 200 OK Content-Type: application/json Content-Length: 58 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 20:57:47 GMT -Partial update is also possible: + {"first_name": "Capitain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"} + +Argh! Thats a typo. the fist name is "Captain", not "Capitain". Let's +correct this: .. code-block:: bash - curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Cap tain"}' http://localhost:5000/users/3/ + curl -i -H "Content-type: application/json" -X PUT -d '{"first_name":"Captain"}' http://localhost:5000/users/3/ HTTP/1.0 200 OK Content-Type: application/json Content-Length: 59 Server: Werkzeug/0.8.3 Python/2.7.2 Date: Mon, 14 Oct 2013 21:08:04 GMT + {"first_name": "Captain", "last_name": "America", "id": 3, "ressource_uri": "/users/3/"} + + +Filtering +--------- + +Ressources can be filtered easily using parameters: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 236 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:07:21 GMT + + {"meta": {"filters": {"last_name": "America"}}, "object_list": + [{"first_name": "Joe", "last_name": "America", "id": 1, + "ressource_uri": "/users/1/"}, {"first_name": "Bob", "last_name": + "America", "id": 3, "ressource_uri": "/users/3/"}] + +Multiple filters are allowed: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America&first_name=Joe" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 171 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:09:32 GMT + + {"meta": {"filters": {"first_name": "Joe", "last_name": "America"}}, + "object_list": [{"first_name": "Joe", "last_name": "America", "id": 1, + "ressource_uri": "/users/1/"}]} Error handling -------------- diff --git a/docs/source/work_with_pagination.rst b/docs/source/work_with_pagination.rst new file mode 100644 index 0000000..eebe89a --- /dev/null +++ b/docs/source/work_with_pagination.rst @@ -0,0 +1,215 @@ +Working with Pagination +======================= + +Creating fixtures +----------------- + +When your address book will be full of entry, you will need to add a +pagination on your API. As it is a common need, REST API Framework +implement a very easy way of doing so. + +Before you can play with the pagination process, you will need to +create more data. You can create those records the way you want: + +* direct insert into the database + +.. code-block:: bash + + sqlite3 adress_book.db + INSERT INTO users VALUES ("Nick", "Furry", 6); + +* using the datastore directly + +.. code-block:: python + + store = SQLiteDataStore({"name": "adress_book.db", "table": "users"}, UserModel) + store.create({"first_name": "Nick", "last_name": "Furry"}) + +* using your API + +.. code-block:: python + + curl -i -H "Content-type: application/json" -X POST -d '{"first_name": "Nick", "last_name": "Furry"}' http://localhost:5000/users/ + +each on of those methods have advantages and disavantages but they all +make the work done. For this example, I propose to use the well know +requests package with a script to create a bunch of random records: + +For this to work you need to install resquests : http://docs.python-requests.org/en/latest/user/install/#install + +.. code-block:: python + + import json + import requests + import random + import string + + def get_random(): + return ''.join( + random.choice( + string.ascii_letters) for x in range( + int(random.random() * 20) + ) + ) + + for i in range(200): + requests.post("http://localhost:5000/users/", data=json.dumps({"first_name": get_random(), "last_name": get_random()})) + +Pagination +---------- + +Now your datastore is filled with more than 200 records, it's time to +paginate. To do so import Pagination and change the controller part of +your app. + +.. code-block:: python + + from rest_api_framework.pagination import Pagination + + class UserEndPoint(Controller): + ressource = { + "ressource_name": "users", + "ressource": {"name": "adress_book.db", "table": "users"}, + "model": UserModel, + "datastore": SQLiteDataStore, + "options": {"validators": [UniqueTogether("first_name", "last_name")]} + } + + controller = { + "list_verbs": ["GET", "POST"], + "unique_verbs": ["GET", "PUT", "DElETE"], + "options": {"pagination": Pagination(20)} + } + + view = {"response_class": JsonResponse, + "options": {"formaters": ["add_ressource_uri", remove_id]}} + +and try your new pagination: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 1811 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:32:55 GMT + + {"meta": {"count": 20, "total_count": 802, "next": "?offset=20", + "filters": {}, "offset": 0, "previous": "null"}, "object_list": + [{"first_name": "Captain", "last_name": "America", + "ressource_uri": "/users/1/"}, {"first_name": "Captain", + "last_name": "America", "ressource_uri": "/users/3/"}, + {"first_name": "John", "last_name": "Doe", "ressource_uri": + "/users/4/"}, {"first_name": "arRFOSYZT", "last_name": "", + "ressource_uri": "/users/5/"}, {"first_name": "iUJsYORMuYeMUDy", + "last_name": "TqFpmcBQD", "ressource_uri": "/users/6/"}, + {"first_name": "EU", "last_name": "FMSAbcUJBSBDPaF", + "ressource_uri": "/users/7/"}, {"first_name": "mWAwamrMQARXW", + "last_name": "yMNpEnYOPzY", "ressource_uri": "/users/8/"}, + {"first_name": "y", "last_name": "yNiKP", "ressource_uri": + "/users/9/"}, {"first_name": "s", "last_name": "TRT", + "ressource_uri": "/users/10/"}, {"first_name": "", "last_name": + "zFUaBd", "ressource_uri": "/users/11/"}, {"first_name": "WA", + "last_name": "priJ", "ressource_uri": "/users/12/"}, + {"first_name": "XvpLttDqFmR", "last_name": "liU", "ressource_uri": + "/users/13/"}, {"first_name": "ZhJqTgYoEUzmcN", "last_name": + "KKDqHJwJMxPSaTX", "ressource_uri": "/users/14/"}, {"first_name": + "qvUxiKIATdKdkC", "last_name": "wIVzfDlKCkjkHIaC", + "ressource_uri": "/users/15/"}, {"first_name": "YSSMHxdDQQsW", + "last_name": "UaKCKgKsgEe", "ressource_uri": "/users/16/"}, + {"first_name": "EKLFTPJLKDINZio", "last_name": "nuilPTzHqattX", + "ressource_uri": "/users/17/"}, {"first_name": "SPcDBtmDIi", + "last_name": "MrytYqElXiIxA", "ressource_uri": "/users/18/"}, + {"first_name": "OHxNppXiYp", "last_name": "AUvUXFRPICsJIB", + "ressource_uri": "/users/19/"}, {"first_name": "WBFGxnoe", + "last_name": "KG", "ressource_uri": "/users/20/"}, {"first_name": + "i", "last_name": "ggLOcKPpMfgvVGtv", "ressource_uri": + "/users/21/"}]} + +Browsering Through Paginated objects +------------------------------------ + +Of course you get 20 records but the most usefull part is the meta +key: + +.. code-block:: json + + {"meta": + {"count": 20, + "total_count": 802, + "next": "?offset=20", + "filters": {}, + "offset": 0, + "previous": "null"} + } + +You can use the "next" key to retreive the 20 next rows: + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?offset=20" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 1849 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 11:38:59 GMT + +.. code-block:: json + + {"meta": {"count": 20, "total_count": 802, "next": "?offset=40", + "filters": {}, "offset": 20, "previous": "?offset=0"}, "object_list": + []} + +.. note:: + + The count and offset keywords can be easily changed to match your + needs. pagination class may take an offset_key and count_key + parameters. So if you prefer to use first_id and limit, you can + change your Paginator class to do so: + + .. code-block:: python + + "options": {"pagination": Pagination(20, + offset_key="first_id", + count_key="limit") + + Wich will results in the following: + + .. code-block:: bash + + curl -i "http://localhost:5000/users/" + {"meta": {"first_id": 0, "total_count": 802, "next": "?first_id=20", + "limit": 20, "filters": {}, "previous": "null"}, "object_list": [] + + +Pagination and Filters +---------------------- + +Pagination and filtering play nice together + +.. code-block:: bash + + curl -i "http://localhost:5000/users/?last_name=America" + HTTP/1.0 200 OK + Content-Type: application/json + Content-Length: 298 + Server: Werkzeug/0.8.3 Python/2.7.2 + Date: Tue, 15 Oct 2013 12:14:59 GMT + + {"meta": {"count": 20, + "total_count": 2, + "next": "null", + "filters": {"last_name": "America"}, + "offset": 0, + "previous": "null"}, + "object_list": [ + {"first_name": "Joe", + "last_name": "America", + "ressource_uri": "/users/1/"}, + {"first_name": "Bob", + "last_name": "America", + "ressource_uri": "/users/3/"} + ] + } diff --git a/rest_api_framework/controllers.py b/rest_api_framework/controllers.py index 145d4e7..cd2c317 100644 --- a/rest_api_framework/controllers.py +++ b/rest_api_framework/controllers.py @@ -130,11 +130,22 @@ def paginate(self, request): offset, count = None, None request_kwargs = request.values.to_dict() filters = request_kwargs + objs = self.datastore.get_list(offset=offset, count=count, **filters) + if self.pagination: + total = self.datastore.count(**filters) + meta = self.pagination.get_metadata(count=count, + offset=offset, + total=total, + **filters) + else: + meta = {"filters": {}} + for k, v in filters.iteritems(): + meta["filters"][k] = v - return self.view(objs=objs, status=200) + return self.view(objs=objs, meta=meta, status=200) def get_list(self, request): """ @@ -198,7 +209,11 @@ def update(self, request, identifier): """ obj = self.datastore.get(identifier=identifier) - obj = self.datastore.update(obj, json.loads(request.data)) + try: + obj = self.datastore.update(obj, json.loads(request.data)) + except ValueError: + raise BadRequest + return self.view( objs=obj, status=200) diff --git a/rest_api_framework/datastore/simple.py b/rest_api_framework/datastore/simple.py index 87fd66a..286a5d9 100644 --- a/rest_api_framework/datastore/simple.py +++ b/rest_api_framework/datastore/simple.py @@ -71,6 +71,9 @@ def create(self, data): self.data.append(obj) return obj[self.model.pk_field.name] + def count(self, **kwargs): + return len(self.filter(**kwargs)) + def update(self, obj, data): """ Update a single object diff --git a/rest_api_framework/datastore/sql.py b/rest_api_framework/datastore/sql.py index 9689571..db33f94 100644 --- a/rest_api_framework/datastore/sql.py +++ b/rest_api_framework/datastore/sql.py @@ -69,6 +69,7 @@ def __init__(self, ressource_config, model, **options): conn.close() self.fields = self.model.get_fields() + def get_connector(self): """ return a sqlite3 connection to communicate with the table @@ -86,6 +87,32 @@ def filter(self, **kwargs): kwargs['query'] += ' FROM {0}' return kwargs + def count(self, **data): + cdt = self.build_conditions(data) + if len(cdt) == 0: + query = "SELECT COUNT (*) FROM {0}".format( + self.ressource_config['table']) + else: + cdt = " AND ".join(cdt) + query = "SELECT COUNT (*) FROM {0} WHERE {1}".format( + self.ressource_config['table'], + cdt + ) + cursor = self.get_connector().cursor() + cursor.execute(query) + return cursor.fetchone()[0] + + def build_conditions(self, data): + return [ + ["{0}='{1}'".format( + e[0], e[1]) for e in condition.iteritems() + ][0] for condition in self.get_conditions(data)] + + def get_conditions(self, data): + return [ + {k: v} for k, v in data.iteritems() if k not in ["query", "fields"] + ] + def paginate(self, data, **kwargs): """ paginate the result of filter using ids limits. Obviously, to @@ -93,10 +120,9 @@ def paginate(self, data, **kwargs): receive from the last call on this method. The max number of row this method can give back depend on the paginate_by option. """ - print kwargs + + where_query = self.build_conditions(data) args = [] - where_query = [ - "{0}='{1}'".format(k, v) for k, v in data.iteritems() if k not in ["query", "fields"]] limit = kwargs.pop("end", None) if kwargs.get("start", None): where_query.append(" id >=?") @@ -105,14 +131,14 @@ def paginate(self, data, **kwargs): if len(where_query) > 0: data["query"] += " WHERE " data["query"] += " AND ".join(where_query) + cursor = self.get_connector().cursor() # a hook for ordering data["query"] += " ORDER BY id ASC" if limit: data["query"] += " LIMIT {0}".format(limit) - cursor = self.get_connector().cursor() - print data["query"] + cursor.execute(data["query"].format(self.ressource_config['table']), tuple(args) ) @@ -210,11 +236,12 @@ def update(self, obj, data): cursor = conn.cursor() update = " ,".join(["{0}='{1}'".format(f, v) for f, v in zip(fields, values)]) - query = "update {0} set {1}".format( + query = "update {0} set {1} WHERE {2}={3}".format( self.ressource_config["table"], - update + update, + self.model.pk_field.name, + obj[self.model.pk_field.name] ) - print query cursor.execute(query) conn.commit() conn.close() diff --git a/rest_api_framework/pagination.py b/rest_api_framework/pagination.py index f55cd14..4694979 100644 --- a/rest_api_framework/pagination.py +++ b/rest_api_framework/pagination.py @@ -27,3 +27,29 @@ def paginate(self, request): if count > self.max: count = self.max return offset, count, request_kwargs + + def get_metadata(self, total=0, offset=0, count=0, **filters): + meta = {self.offset_key: offset, + self.count_key: count, + "total_count": total, + "filters": {} + } + for k, v in filters.iteritems(): + meta["filters"][k] = v + if offset == 0: + meta['previous'] = "null" + else: + meta["previous"] = offset - count + if meta["previous"] < 0: + meta["previous"] = 0 + if meta['previous'] != "null": + meta["previous"] = "?{0}={1}".format(self.offset_key, + meta["previous"]) + + meta["next"] = offset + count + if meta["next"] > total: + meta["next"] = "null" + if meta['next'] != "null": + meta["next"] = "?{0}={1}".format(self.offset_key, + meta["next"]) + return meta diff --git a/rest_api_framework/views.py b/rest_api_framework/views.py index 47c37a3..eed2efd 100644 --- a/rest_api_framework/views.py +++ b/rest_api_framework/views.py @@ -28,18 +28,31 @@ def __init__(self, model, ressource_name, def __call__(self, *args, **kwargs): + meta = None + + if "meta" in kwargs: + meta = kwargs.pop("meta") + if "objs" in kwargs: objs = self.format(kwargs.pop('objs')) - return Response(json.dumps(objs), + if meta: + response = {"meta": meta, + "object_list": objs} + else: + response = objs + return Response(json.dumps(response), mimetype="application/json", **kwargs) else: - return Response(*args, - mimetype="application/json", - **kwargs) + response = "" + if args: + response = json.dumps(*args) + return Response(response, + mimetype="application/json", + **kwargs) def make_options(self, **options): - print options + pass def format(self, objs): diff --git a/setup.cfg b/setup.cfg index 7b58525..2268c04 100644 --- a/setup.cfg +++ b/setup.cfg @@ -7,4 +7,5 @@ with-coverage=1 cover-xml=1 cover-xml-file=report_coverage.xml cover-package=rest_api_framework -stop=1 \ No newline at end of file +stop=1 +nocapture=1 \ No newline at end of file diff --git a/tests/controllers_test.py b/tests/controllers_test.py index 14da42c..65d95cb 100644 --- a/tests/controllers_test.py +++ b/tests/controllers_test.py @@ -65,8 +65,7 @@ def test_get_list(self): response_wrapper=BaseResponse) resp = client.get("/address/") self.assertEqual(resp.status_code, 200) - print resp.data - self.assertIsInstance(json.loads(resp.data), list) + self.assertIsInstance(json.loads(resp.data)["object_list"], list) def test_get(self): client = Client(WSGIDispatcher([ApiApp]), @@ -230,7 +229,8 @@ def test_base_pagination(self): client = Client(WSGIDispatcher([ApiApp]), response_wrapper=BaseResponse) resp = client.get("/address/") - self.assertEqual(len(json.loads(resp.data)), 20) + + self.assertEqual(len(json.loads(resp.data)["object_list"]), 20) def test_base_pagination_count(self): client = Client(WSGIDispatcher([ApiApp]), @@ -242,13 +242,13 @@ def test_base_pagination_count_overflow(self): client = Client(WSGIDispatcher([ApiApp]), response_wrapper=BaseResponse) resp = client.get("/address/?count=200") - self.assertEqual(len(json.loads(resp.data)), 20) + self.assertEqual(len(json.loads(resp.data)["object_list"]), 20) def test_base_pagination_offset(self): client = Client(WSGIDispatcher([ApiApp]), response_wrapper=BaseResponse) resp = client.get("/address/?offset=2") - self.assertEqual(json.loads(resp.data)[0]['ressource_uri'], + self.assertEqual(json.loads(resp.data)["object_list"][0]['ressource_uri'], "/address/2/") @@ -261,7 +261,7 @@ def test_base_pagination(self): client.post("/address/", data=json.dumps({"name": "bob", "age": 34})) resp = client.get("/address/") - self.assertEqual(len(json.loads(resp.data)), 20) + self.assertEqual(len(json.loads(resp.data)["object_list"]), 20) os.remove("test.db") def test_base_pagination_offset(self): @@ -271,7 +271,7 @@ def test_base_pagination_offset(self): client.post("/address/", data=json.dumps({"name": "bob", "age": 34})) resp = client.get("/address/?offset=2") - self.assertEqual(json.loads(resp.data)[0]['id'], 2) + self.assertEqual(json.loads(resp.data)["object_list"][0]['id'], 2) os.remove("test.db") def test_base_pagination_count(self): @@ -292,7 +292,7 @@ def test_base_pagination_count_offset(self): data=json.dumps({"name": "bob", "age": 34})) resp = client.get("/address/?count=2&offset=4") self.assertEqual(len(json.loads(resp.data)), 2) - self.assertEqual(json.loads(resp.data)[0]['id'], 4) + self.assertEqual(json.loads(resp.data)["object_list"][0]['id'], 4) os.remove("test.db") @@ -303,7 +303,7 @@ def test_get_partial_list(self): response_wrapper=BaseResponse) resp = client.get("/address/?fields=age") # we only want "age". get_list add id, JsonResponse add ressource_uri - self.assertEqual(len(json.loads(resp.data)[0].keys()), 3) + self.assertEqual(len(json.loads(resp.data)["object_list"][0].keys()), 3) def test_get_partial_raise(self): client = Client(WSGIDispatcher([PartialApiApp]), @@ -322,8 +322,7 @@ def test_get_partial_sql(self): resp = client.get("/address/?fields=age") # we only want "age". get_list add id, JsonResponse add ressource_uri - print resp - self.assertEqual(len(json.loads(resp.data)[0].keys()), 3) + self.assertEqual(len(json.loads(resp.data)["object_list"][0].keys()), 3) os.remove("test.db") def test_get_partial_sql_raise(self): diff --git a/tests/datastore_test.py b/tests/datastore_test.py index 51c55e3..dfd2ba3 100644 --- a/tests/datastore_test.py +++ b/tests/datastore_test.py @@ -244,7 +244,6 @@ def test_pagination(self): ApiModel) for i in range(100): store.create({"name": "bob", "age": 34}) - print store.get_list(count=10) self.assertEqual(len(store.get_list(count=10)), 10) self.assertEqual(store.get_list(count=10)[-1]["id"], 10) self.assertEqual(store.get_list(offset=15)[0]["id"], 15) @@ -279,7 +278,6 @@ def test_create(self): self.assertEqual(store.create({"name": "bob", "age": 34}), 1) self.assertEqual(store.create({"name": "bob", "age": 35}), 2) self.assertEqual(store.create({"name": "bob", "age": 34}), 3) - print store.get(3) self.assertEqual(store.get(3)["id"], 3) os.remove("test.db")