[docs]classWSGIDispatcher(DispatcherMiddleware):""" Embed multiple endpoint in one
diff --git a/docs/build/_sources/index.txt b/docs/build/_sources/index.txt
index 7245871..d4c2c38 100644
--- a/docs/build/_sources/index.txt
+++ b/docs/build/_sources/index.txt
@@ -12,6 +12,7 @@ Contents:
:maxdepth: 2
introduction
+ tutorial
datastore
controller
pagination
diff --git a/docs/build/_sources/tutorial.txt b/docs/build/_sources/tutorial.txt
new file mode 100644
index 0000000..1fa00ea
--- /dev/null
+++ b/docs/build/_sources/tutorial.txt
@@ -0,0 +1,128 @@
+Tutorial building an adressebook API
+====================================
+
+First Step Building a user endpoint
+-----------------------------------
+
+For this project we need users. Users will be helpfull for our adress
+book and for our authentication process.
+
+Users will be define with at least a first name and a last name. We
+also need an unique identifier to retreive the user.
+
+Define a model
+~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ from rest_api_framework import models
+
+ class UserModel(models.Model):
+
+ fields = [models.StringField(name="first_name", required=True),
+ models.StringField(name="last_name", required=True),
+ models.PkField(name="id", required=True)
+ ]
+
+The use of required_true will ensure that a user without this field
+cannot be created
+
+Chose a DataStore
+~~~~~~~~~~~~~~~~~
+
+We also need a datastore to get a place where we can save our
+users. For instance we will use a sqlite3 database. The
+SQLiteDataStore is what we need
+
+.. code-block:: python
+
+ from rest_api_framework.datastore import SQLiteDataStore
+
+Chose a view
+~~~~~~~~~~~~
+
+We want results to be rendered as Json. We use the JsonResponse view
+for that:
+
+.. code-block:: python
+
+ from rest_api_framework.views import JsonResponse
+
+Create The user endpoint
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To create an endpoint, we need a controller. This will manage our
+endpoint in a RESTFUL fashion.
+
+.. code-block:: python
+
+ from rest_api_framework.controllers import Controller
+
+ class UserEndPoint(Controller):
+ ressource = {
+ "ressource_name": "users",
+ "ressource": {"name": "adress_book.db", "table": "users"},
+ "model": UserModel,
+ "datastore": SQLiteDataStore
+ }
+
+ controller = {
+ "list_verbs": ["GET", "POST"],
+ "unique_verbs": ["GET", "PUT", "DElETE"]
+ }
+
+ view = {"response_class": JsonResponse}
+
+then we must run our application:
+
+.. code-block:: python
+
+ 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)
+
+Summary
+~~~~~~~
+
+So far, all of the code should look like this:
+
+.. 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
+
+
+ class UserModel(models.Model):
+
+ fields = [models.StringField(name="first_name", required=True),
+ models.StringField(name="last_name", required=True),
+ models.PkField(name="id", required=True)
+ ]
+
+
+ class UserEndPoint(Controller):
+ ressource = {
+ "ressource_name": "users",
+ "ressource": {"name": "adress_book.db", "table": "users"},
+ "model": UserModel,
+ "datastore": SQLiteDataStore
+ }
+
+ controller = {
+ "list_verbs": ["GET", "POST"],
+ "unique_verbs": ["GET", "PUT", "DElETE"]
+ }
+
+ view = {"response_class": JsonResponse}
+
+ 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)
+
+Next: :doc:`using_user_endpoint`
diff --git a/docs/build/_sources/using_user_endpoint.txt b/docs/build/_sources/using_user_endpoint.txt
new file mode 100644
index 0000000..c797111
--- /dev/null
+++ b/docs/build/_sources/using_user_endpoint.txt
@@ -0,0 +1,62 @@
+Playing with the newly created endpoint
+=======================================
+
+First you can check that your endpoint is up
+
+.. code-block:: bash
+
+ curl -i "http://localhost:5000/users/"
+
+ HTTP/1.0 200 OK
+ Content-Type: application/json
+ Content-Length: 2
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 12:52:22 GMT
+
+Your endpoint is responding but does not have any data. Let's add
+some:
+
+Create a user
+-------------
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"John", "last_name": "Doe"}' http://localhost:5000/users/
+
+ HTTP/1.0 201 CREATED
+ Location: http://localhost:5000/users/1
+ Content-Type: application/json
+ Content-Length: 0
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:00:13 GMT
+
+Error handling
+~~~~~~~~~~~~~~
+
+If you don't provide a last_name, the API will raise a BAD REQUEST
+explaining your error:
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"John"}' http://localhost:5000/users/
+
+ HTTP/1.0 400 BAD REQUEST
+ Content-Type: application/json
+ Content-Length: 62
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:21:10 GMT
+
+ {"error": "last_name is missing. Cannot create the ressource"}
+
+The same apply if you dont give coherent data:
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":45, "last_name": "Doe"}' http://localhost:5000/users/
+
+ HTTP/1.0 400 BAD REQUEST
+ Content-Type: application/json
+ Content-Length: 41
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:24:53 GMT
+ {"error": "first_name does not validate"}
diff --git a/docs/build/datastore.html b/docs/build/datastore.html
index 545d844..44a159b 100644
--- a/docs/build/datastore.html
+++ b/docs/build/datastore.html
@@ -24,8 +24,8 @@
-
-
+
+
Change kwargs[“query”] with “WHERE X=Y statements”. The
filtering will be done with the actual evaluation of the query
-in paginate() the sql can then be lazy
Retreive the object to be updated
-(get() will raise a NotFound error if
+(get() will raise a NotFound error if
the row does not exist)
Validate the fields to be updated and return the updated row
diff --git a/docs/build/searchindex.js b/docs/build/searchindex.js
index 6a8faaa..5365f7c 100644
--- a/docs/build/searchindex.js
+++ b/docs/build/searchindex.js
@@ -1 +1 @@
-Search.setIndex({envversion:42,terms:{represent:[2,7,5],all:[2,4,5,7],code:[],list_verb:[2,1,4],follow:2,sould:[7,5],row:[7,5],decid:2,typeerror:[],depend:[2,4,5,7],authoris:2,tweet:[2,7,5],send:[2,7,5],decis:2,get_connector:[7,5],islic:[],ressource_list:[],sourc:[7,5],string:5,fals:2,util:2,verb:[2,4,5],mechan:1,objec:[],veri:[2,6,3],hanld:2,retrrreiv:[],list:[2,6,3,5,7],"try":3,"20th":1,naiv:[7,5],rate:[2,4],design:[],chek:[7,5],compat:7,index:[0,5],what:[],neg:[],abl:[2,1,5,4,7],access:[2,3],delet:[2,1,5,4,7],"new":[2,7,5],"public":3,can:[1,2,3,5,6,7],hash:3,behavior:2,gener:[],unauthor:[3,5],here:[],behaviour:[],modif:[],address:2,eventapp:6,modifi:2,valu:[2,1,3],search:0,permit:5,codebas:2,bob:2,environn:6,control:[],useful:2,extra:7,app:[2,6,5],from:[],describ:[],fron:[7,5],regist:5,next:[],live:2,call:[2,7,5],type:[2,7,5],tell:[2,4],more:[],wrapper:[7,5],actuali:[2,7,5],under:6,notic:6,"__iter__":[],particular:[2,3],actual:[7,5],must:[2,3,7],none:[3,5,7],endpoint:[],alia:[],prepar:[7,5],work:[1,5,3,7],uniqu:[2,4,5],firstapp:5,obvious:[7,5],whatev:[2,7],learn:2,purpos:3,root:[4,5],def:3,overrid:7,sqlite:[2,7,5],quickstart:[],give:[2,1,5,7],process:5,accept:[],topic:[2,4],want:[2,1,5,7],paginated_bi:7,alwai:[1,3],multipl:[],secur:2,anoth:[2,3,7],write:[6,4],how:[],instead:[2,1],csv:[2,7],simpl:7,updat:[2,7,5],map:5,resourc:2,max:[4,7],after:[7,5],mai:[2,6,7],datastor:[],data:[2,3,5,7],bind:[],author:[],correspond:2,inform:[2,1,5,7],allow:[2,4],callabl:5,talk:6,oper:[7,5],soon:[2,7,5],paramet:[2,7],render:2,fit:2,trootl:2,main:[2,5],them:[2,5],good:2,"return":[2,1,5,3,7],"__getitem__":[],handl:[2,4],auto:5,auth:[3,5],number:[2,7,5],now:[3,5,7],nor:[],enabl:[2,1],choic:1,somewher:3,name:[2,6,3,5,7],anyth:[2,3,5,7],authent:[],wole:[7,5],easili:[2,3],token:[2,1],each:[1,2,3,4,5,6,7],fulli:[2,3],unique_uri:5,mean:2,compil:2,domain:1,unique_verb:[2,1,4],connect:[3,5,7],our:[2,3],meth:[],todo:[],event:6,special:[2,4],content:[0,7],rewrit:[],rel:[],sqlitedatastor:[2,7],wich:[2,4],integr:2,earlier:[],free:[2,7,5],base:[2,1,7],mime:2,scallabl:2,put:[2,1,4],care:7,reusabl:2,launch:6,could:[3,5,7],keep:2,filter:[2,1,5,7],mvc:2,pagin:[2,4,7],think:7,first:2,feed:2,rang:2,dont:2,directli:1,feel:5,onc:[2,7],sqldatastor:7,placehold:3,hook:2,alreadi:2,done:[2,7,5],messag:2,count_kei:[1,5],primari:[7,5],given:3,interact:2,construct:5,get_list:[7,5],too:[2,3,5,7],statement:[7,5],"final":7,store:[2,7],schema:[],option:7,wsgiwrapp:5,part:2,than:[],serv:[2,6],notfound:[3,5,7],keyword:[1,4],provid:[2,3,7],remov:[1,5,7],bee:7,reus:[2,7],ressourc:[],str:[],apikeyauthent:3,argument:2,have:[1,2,3,5,6,7],"__main__":2,need:[2,1,5,3,7],check_auth:[3,5],outbound:[],load_url:5,dictionnari:[7,5],self:[3,5,7],client:2,note:[],also:[2,7],without:5,take:[2,6,1,5,7],instanci:[1,5,3,7],singl:[],even:2,integerfield:[2,7],object:[2,3,5,7],dialogu:4,"class":[1,2,3,5,6,7],ressouc:2,url:[2,4,5],gather:[],flow:2,uri:5,doe:[2,7,5],ext:1,unique_id:4,clean:2,pattern:2,constrain:[7,5],jsonrespons:2,apiapp:[2,6],wsgi:5,show:2,text:[7,5],syntax:5,fine:3,xml:2,moer:2,onli:[1,2,3,4,5,6,7],apimodel:[2,7,5],run_simpl:[2,6],configur:[2,1],should:[1,2,3,4,5,7],dict:[2,1,5,3,7],local:2,variou:[],get:[1,2,3,4,5,7],stop:[],becaus:[1,4],get_us:3,cannot:[2,7],other_paramet:[],requir:[2,3,5,7],retreiv:[2,7,5],userapp:6,emb:[],dispatch_request:5,ressoourc:2,yield:[],method:[2,7],stuff:4,common:2,contain:[2,7,5],orign:1,where:[],view:[],set:[2,7,5],see:[1,4,7],mandatori:7,result:[2,1,5,4,7],arg:5,close:[7,5],page:[2,0],extend:2,databas:[],someth:[3,7],unauthoriez:[],response_class:2,between:[2,4,7],"import":[2,6],attribut:[1,7],accord:[7,5],kei:[3,5,7],secondapp:5,itertool:[],extens:2,lazi:[7,5],come:7,protect:3,last:[2,7,5],easi:[2,6,7],howev:6,etc:2,instanc:[2,6],grain:3,basestr:[7,5],mani:2,com:2,"30th":1,point:[7,5],dispatch:5,loader:5,header:2,path:[],backend:[],evalu:[7,5],been:[2,7,5],wsgidispatch:[2,6,5],json:2,basic:5,validate_field:[7,5],immedi:7,convert:[7,5],ani:[2,3],understand:2,those:[2,1,7],"case":2,look:[2,6,7,5],mount:5,properti:2,act:[2,5],defin:[2,1,5,4,7],calcul:1,apikei:3,elt:[],abov:1,error:[3,5,7],"na\u00efv":[],multiple_endpoint:[],pythonlistdatastor:[2,3,5],azerti:3,loos:[7,5],finnali:3,advantag:[7,5],applic:[2,1,5],implment:2,dictonari:2,kwarg:[1,5,7],"__init__":[3,5,7],present:[2,5],thei:[2,3,5,4,7],grant:3,perform:[3,5],make:[2,4,5],same:2,sqlite3:[2,7,5],apiappauth:3,complet:7,http:[1,5],pouet:[],hanl:2,action:[2,3,5],rais:[3,5,7],user:[2,6,3,5],respons:[2,5],implement:[2,4,7],whole:1,well:7,inherit:[2,1,5,7],exampl:[],thi:[1,2,3,4,5,7],fiter:[7,5],apikeyauthor:3,rout:[2,5],usual:2,authmodel:3,identifi:[2,3,5,4,7],just:[2,3,5,7],distant:2,expos:2,hint:[],kawrg:5,except:[3,5,7],param:5,exempl:[2,1,4],add:[2,6,7],other:[2,7],els:3,save:[2,7,5],modul:0,match:[7,5],build:[2,1,5],real:[7,5],indentifi:3,ressource_config:[7,5],format:[2,7],read:[2,3,4],pkfield:[2,7,5],password:3,like:[2,6,1,4,7],integ:[7,5],either:[2,7,5],authorized_method:[],manag:[2,4],twitter:[2,7],creation:[7,5],some:[2,1,5,4,7],back:[7,5],use_reload:[2,6],definit:[],totali:2,werkzeug:[2,6,5],refer:[],machin:2,run:2,stringfield:[2,3,7],autoincr:[7,5],step:[],offset:[4,7],"__name__":2,post:[2,1,5,4],about:[2,6,1,7],obj:[7,5],memori:[2,7,5],apicontrol:5,constructor:6,disabl:2,block:[],subset:[7,5],paginate_bi:[7,5],own:[2,1,5,4,7],"float":[7,5],empti:1,chang:[2,7,5],your:[],to_dict:3,accordingli:5,wai:[2,3,5,7],support:[7,5],use_debugg:[2,6],custom:[3,7],start:[2,1,5,7],suit:[2,7],"function":3,properli:[7,5],max_result:5,form:5,tupl:5,link:7,"true":[2,6,3,7],count:[4,7],commun:[2,7,5],made:5,possibl:[6,1,5,7],displai:2,below:1,limit:[2,1,5,4,7],otherwis:[2,3,5],problem:1,unrel:2,datastoreinst:2,featur:4,creat:[],"int":[7,5],request:[5,1,4,3],"abstract":5,repres:[7,5],clever:[],exist:[2,3,5,7],file:[7,5],curl:1,offset_kei:[1,5],check:[2,3,5,7],herit:5,automaticali:5,when:[2,1,4],detail:4,rest_api_framework:[2,6,1,5,7],field:[],valid:[],test:[2,7,5],you:[1,2,3,5,6,7],ressource_nam:[2,4],architectur:[],relat:[4,7],finali:2,multimpl:6,developp:6,queri:[7,5],sql:[2,7,5],endoint:6,receiv:[1,5,7],dog:1,descript:[],time:[3,5,7],far:6},objtypes:{"0":"py:module","1":"py:method","2":"py:class"},objnames:{"0":["py","module","Python module"],"1":["py","method","Python method"],"2":["py","class","Python class"]},filenames:["index","pagination","introduction","authentication","controller","references","multiple_endpoint","datastore"],titles:["Welcome to Python Rest Api Framework’s documentation!","Paginate a ressource","What is Python REST API Framework","Authentication and Authorization","Controllers","REST API Framework API references","Using more than a single endpoint","Datastore"],objects:{"rest_api_framework.controllers.ApiController":{index:[5,1,1,""],get:[5,1,1,""],create:[5,1,1,""],paginate:[5,1,1,""],update:[5,1,1,""],get_list:[5,1,1,""],unique_uri:[5,1,1,""],"delete":[5,1,1,""]},"rest_api_framework.controllers.Dispatcher":{load_urls:[5,1,1,""],dispatch_request:[5,1,1,""]},"rest_api_framework.datastore.simple":{PythonListDataStore:[5,2,1,""]},"rest_api_framework.pagination":{Pagination:[5,2,1,""]},rest_api_framework:{controllers:[5,0,0,"-"]},"rest_api_framework.datastore.simple.PythonListDataStore":{get_list:[5,1,1,""],update:[5,1,1,""],get:[5,1,1,""]},"rest_api_framework.authentication.Authentication":{check_auth:[5,1,1,""]},"rest_api_framework.pagination.Pagination":{paginate:[5,1,1,""]},"rest_api_framework.controllers":{Controller:[5,2,1,""],WSGIDispatcher:[5,2,1,""],WSGIWrapper:[5,2,1,""],Dispatcher:[5,2,1,""],ApiController:[5,2,1,""]},"rest_api_framework.authentication":{Authentication:[5,2,1,""]}},titleterms:{control:[2,4,5],quickstart:2,creat:7,argument:4,rest:[2,0,5],api:[2,0,5],mandatori:4,tabl:0,your:7,paramet:4,what:2,from:2,describ:7,field:2,databas:2,avail:7,how:[2,3],valid:2,sqlitedatastor:5,ressourc:[2,1,7],document:0,simpl:[2,5],method:1,singl:6,refer:5,model:[2,7],option:[2,4],python:[2,0],max:1,here:2,backend:3,framework:[2,0,5],base:5,indic:0,offset:1,datastor:[2,7,5],than:6,welcom:0,count:1,endpoint:6,author:3,authent:[2,3,5],more:6,exampl:3,pagin:[1,5],implement:1,architectur:2,where:2,other:1,view:2}})
\ No newline at end of file
+Search.setIndex({envversion:42,terms:{represent:[3,8,6],all:[3,5,6,8,9],code:9,list_verb:[3,1,5,9],follow:3,sould:[8,6],row:[8,6],decid:3,typeerror:[],depend:[3,5,6,8],authoris:3,tweet:[3,8,6],send:[3,8,6],decis:3,get_connector:[8,6],islic:[],ressource_list:[],sourc:[8,6],string:6,fals:3,util:3,verb:[3,5,6],mechan:1,objec:[],veri:[3,7,4],hanld:3,retrrreiv:[],list:[3,7,4,6,8],last_nam:[2,9],"try":4,adress_book:9,"20th":1,naiv:[8,6],rate:[3,5],design:[],pass:6,chek:[8,6],integr:3,compat:8,index:[0,6],what:[],neg:[],abl:[3,1,6,5,8],access:[3,4],delet:[1,3,5,6,8,9],"new":[3,8,6],"public":4,can:[2,1,3,4,6,7,8,9],hash:4,behavior:3,gener:[],unauthor:4,here:[],behaviour:[],modif:[],address:3,eventapp:7,modifi:3,valu:[3,1,4],search:0,permit:6,codebas:3,bob:3,explain:2,environn:7,control:[],useful:3,extra:8,appli:2,app:[3,7,6,9],from:[],describ:[],fron:[8,6],regist:[],next:9,live:3,call:[3,8,6],type:[3,2,8,6],tell:[3,5],more:[],wrapper:[8,6],actuali:[3,8,6],under:7,notic:7,"__iter__":[],particular:[3,4],actual:[8,6],must:[3,4,6,8,9],none:[4,6,8],endpoint:[],alia:[],prepar:[8,6],work:[1,6,4,8],uniqu:[3,5,6,9],firstapp:6,obvious:[8,6],whatev:[3,8],learn:3,purpos:4,root:[5,6],def:4,overrid:8,sqlite:[3,8,6],quickstart:[],give:[3,2,1,6,8],process:[6,9],accept:[],topic:[3,5],want:[3,1,6,8,9],paginated_bi:8,alwai:[1,4],multipl:[],first_nam:[2,9],secur:3,anoth:[3,4,8],write:[7,5],how:[],instead:[3,1],csv:[3,8],simpl:[],updat:[3,8,6],map:6,resourc:3,max:5,after:[8,6],mai:[3,7,8],datastor:[],data:[3,2,4,6,8],bind:[],author:[],correspond:3,inform:[3,1,6,8],environ:6,allow:[3,5],callabl:6,talk:7,oper:[8,6],least:9,soon:[3,8,6],userendpoint:9,paramet:3,render:[3,9],fit:3,trootl:3,main:[3,6],them:[3,6],good:3,"return":[3,1,6,4,8],"__getitem__":[],handl:[3,5],auto:[],auth:[4,6],number:[3,8,6],now:[4,6,8],nor:[],enabl:[3,1,6],choic:1,somewher:4,name:[3,4,6,7,8,9],anyth:[3,4,6,8],authent:[],wole:[8,6],easili:[3,4],token:[3,1],each:[1,3,4,5,6,7,8],fulli:[3,4],unique_uri:6,mean:3,compil:3,domain:1,unique_verb:[3,1,5,9],connect:[4,6,8],our:[3,4,9],meth:[],todo:[],event:7,special:[3,5],miss:2,newli:9,content:[0,8,2],rewrit:[],rel:[],sqlitedatastor:3,wich:[3,5],to_dict:4,earlier:[],free:[3,8,6],base:[3,1],mime:3,scallabl:3,put:[3,1,5,9],care:8,reusabl:3,launch:7,could:[4,6,8],keep:3,filter:[3,1,6,8],length:2,mvc:3,pagin:[3,5],think:8,first:3,feed:3,rang:3,dont:[3,2],directli:1,feel:6,onc:[3,8],sqldatastor:8,placehold:4,hook:3,alreadi:3,done:[3,8,6],messag:3,count_kei:[1,6],primari:[8,6],given:4,interact:3,construct:6,get_list:[8,6],too:[3,4,6,8],statement:[8,6],john:2,"final":8,store:[3,8],schema:[],option:[],wsgiwrapp:6,part:3,than:[],serv:[3,7,9],notfound:[4,6,8],keyword:[1,5],provid:[3,2,4,8],remov:[1,6,8],bee:8,project:9,reus:[3,8],ressourc:[],str:[],make_opt:6,fashion:9,apikeyauthent:4,argument:3,have:[2,1,3,4,6,7,8],"__main__":[3,9],need:[1,3,4,6,8,9],check_auth:[4,6],date:2,load_url:6,dictionnari:[8,6],self:[4,6,8],client:3,note:[],also:[3,8,9],without:[6,9],take:[3,7,1,6,8],instanci:[1,6,4,8],singl:[],even:3,integerfield:[3,8],object:[3,4,6,8],dialogu:5,plai:9,"class":[1,3,4,6,7,8,9],ressouc:3,don:2,url:[3,5,6],gather:[],flow:3,uri:6,doe:[3,2,8,6],ext:1,unique_id:5,clean:3,pattern:3,constrain:[8,6],jsonrespons:[3,9],apiapp:[3,7],wsgi:6,show:3,text:[8,6],syntax:6,fine:4,xml:3,moer:3,onli:[1,3,4,5,6,7,8],locat:2,apimodel:[3,8,6],run_simpl:[3,7,9],configur:[3,1],should:[1,3,4,5,6,8,9],dict:[3,1,6,4,8],local:3,oct:2,variou:[],get:[1,3,4,5,6,8,9],stop:[],becaus:[1,5],get_us:[4,6],cannot:[3,2,8,9],other_paramet:[],requir:[3,4,6,8,9],retreiv:[3,8,6,9],userapp:7,emb:[],dispatch_request:6,ressoourc:3,yield:[],method:3,book:9,bad:2,stuff:5,common:3,contain:[3,8,6],orign:1,where:[],view:[],respond:2,set:[3,8,6],see:[1,5,8],mandatori:[],result:[1,3,5,6,8,9],arg:6,close:[8,6],extend:3,databas:[],someth:[4,8],unauthoriez:[],response_class:[3,9],between:[3,5,8],"import":[3,7,9],attribut:[1,8],accord:[8,6],kei:[4,6,8],secondapp:6,itertool:[],extens:3,lazi:[8,6],come:8,protect:4,last:[3,8,6,9],easi:[3,7,8],howev:7,etc:3,instanc:[3,7,9],grain:4,basestr:[8,6],let:2,com:3,"30th":1,point:[8,6],dispatch:6,loader:6,header:3,path:[],usermodel:9,wsgi_app:6,backend:[],evalu:[8,6],been:[3,8,6],wsgidispatch:[3,7,6,9],json:[3,2,9],basic:6,validate_field:[8,6],immedi:8,convert:[8,6],ani:[3,2,4],understand:3,required_tru:9,those:[3,1,8],"case":3,look:[3,7,8,6,9],mount:6,properti:3,act:[3,6],defin:[3,1,5],calcul:1,apikei:4,elt:[],abov:1,error:4,"na\u00efv":[],multiple_endpoint:[],pythonlistdatastor:[3,4,6],azerti:4,loos:[8,6],finnali:4,coher:2,advantag:[8,6],applic:[3,2,1,6,9],implment:3,dictonari:3,ratelimit:6,kwarg:[1,6,8],"__init__":[4,6,8],present:[3,6],thei:[3,4,6,5,8],grant:4,perform:4,make:[3,5,6],same:[3,2],tutori:[],sqlite3:[3,8,6,9],apiappauth:4,complet:8,http:[2,1,6],pouet:[],hanl:3,action:[3,4],rais:[2,4,6,8],user:[3,7,4],mani:3,outbound:[],respons:[3,6],implement:[3,5],whole:1,well:8,inherit:[3,1,6,8],exampl:[],thi:[1,3,4,5,6,8,9],fiter:[8,6],apikeyauthor:4,rout:[3,6],usual:3,authmodel:4,identifi:[3,5,4,6,8,9],just:[3,4,6,8],distant:3,expos:3,hint:[],kawrg:[],except:[4,6,8],param:6,exempl:[3,1,5],add:[3,2,8,7],other:3,els:4,save:[3,8,6,9],adress:9,modul:0,match:[8,6],build:[3,1],real:[8,6],indentifi:4,ressource_config:[8,6],format:[3,8],read:[3,4,5],pkfield:[3,8,6,9],mon:2,gmt:2,password:4,helpful:9,like:[1,3,5,7,8,9],integ:[8,6],server:2,either:[3,8,6],authorized_method:[],apicontrol:6,twitter:[3,8],creation:[8,6],some:[2,1,3,5,6,8],back:[8,6],use_reload:[3,7,9],definit:[],totali:3,werkzeug:[3,2,6,9,7],adressebook:[],localhost:2,refer:[],machin:3,run:[3,9],stringfield:[3,4,8,9],autoincr:[8,6],step:[],offset:5,"__name__":[3,9],post:[2,1,3,5,6,9],about:[3,7,1,8],obj:[8,6],memori:[3,8,6],page:[3,0],constructor:7,disabl:3,block:[],subset:[8,6],paginate_bi:[8,6],own:[3,1,6,5,8],"float":[8,6],empti:1,ensur:9,chang:[3,8,6],your:[],manag:[3,5,6,9],accordingli:6,wai:[3,4,6,8],support:[8,6],use_debugg:[3,7,9],custom:[4,8],start:[3,1,6,8],suit:[3,8],"function":4,properli:[8,6],max_result:6,form:6,tupl:6,link:8,"true":[3,7,4,8,9],count:5,commun:[3,8,6],made:6,possibl:[7,1,6,8],start_respons:6,displai:3,below:1,limit:[3,1,6,5,8],otherwis:[3,4,6],problem:1,unrel:3,datastoreinst:3,featur:5,creat:[],"int":[8,6],request:[2,1,5,4,6],"abstract":6,repres:[8,6],clever:[],exist:[3,4,6,8],file:[8,6],curl:[2,1],offset_kei:[1,6],check:[3,2,4,6,8],herit:6,automaticali:6,when:[3,1,5],detail:5,rest_api_framework:[1,3,6,7,8,9],field:[],valid:[],test:[3,8,6],you:[2,1,3,4,6,7,8],ressource_nam:[3,5,9],architectur:[],relat:[5,8],finali:3,multimpl:7,developp:7,queri:[8,6],sql:[3,8,6],endoint:7,receiv:[1,6,8],dog:1,descript:[],place:9,time:[4,6,8],far:[7,9]},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","authentication","controller","references","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","Authentication and Authorization","Controllers","REST API Framework API references","Using more than a single endpoint","Datastore","Tutorial building an adressebook API"],objects:{"rest_api_framework.datastore.sql.SQLiteDataStore":{get:[8,2,1,""],get_connector:[8,2,1,""],create:[8,2,1,""],paginate:[8,2,1,""],update:[8,2,1,""],get_list:[8,2,1,""],filter:[8,2,1,""],"delete":[8,2,1,""]},"rest_api_framework.datastore.base.DataStore":{get:[8,2,1,""],create:[8,2,1,""],paginate:[8,2,1,""],update:[8,2,1,""],get_list:[8,2,1,""],filter:[8,2,1,""],validate_fields:[8,2,1,""],validate:[8,2,1,""],"delete":[8,2,1,""]},"rest_api_framework.controllers.ApiController":{index:[6,2,1,""],get:[6,2,1,""],create:[6,2,1,""],paginate:[6,2,1,""],update:[6,2,1,""],get_list:[6,2,1,""],unique_uri:[6,2,1,""],"delete":[6,2,1,""]},"rest_api_framework.datastore.simple.PythonListDataStore":{get:[6,2,1,""],update:[6,2,1,""],get_list:[6,2,1,""]},"rest_api_framework.datastore.sql":{SQLiteDataStore:[8,1,1,""]},"rest_api_framework.datastore.simple":{PythonListDataStore:[6,1,1,""]},"rest_api_framework.controllers":{Controller:[6,1,1,""],WSGIWrapper:[6,1,1,""],ApiController:[6,1,1,""],WSGIDispatcher:[6,1,1,""]},rest_api_framework:{controllers:[6,0,0,"-"]},"rest_api_framework.datastore.base":{DataStore:[8,1,1,""]},"rest_api_framework.controllers.Controller":{load_urls:[6,2,1,""],make_options:[6,2,1,""]},"rest_api_framework.authentication.Authentication":{get_user:[6,2,1,""]},"rest_api_framework.pagination.Pagination":{paginate:[6,2,1,""]},"rest_api_framework.controllers.WSGIWrapper":{dispatch_request:[6,2,1,""],wsgi_app:[6,2,1,""]},"rest_api_framework.pagination":{Pagination:[6,1,1,""]},"rest_api_framework.authentication":{Authentication:[6,1,1,""]}},titleterms:{control:[3,5,6],quickstart:3,creat:[2,8,9],chose:9,argument:5,rest:[3,0,6],api:[3,0,6,9],mandatori:5,tabl:0,user:[2,9],your:8,paramet:5,what:3,from:3,describ:8,how:[3,4],databas:3,newli:2,summari:9,avail:8,field:3,valid:3,build:9,adressebook:9,sqlitedatastor:6,ressourc:[3,1,8],document:0,simpl:[3,6],method:1,singl:7,refer:6,model:[3,8,9],option:[3,5],python:[3,0],max:1,first:9,here:3,backend:4,framework:[3,0,6],step:9,base:6,indic:0,offset:1,datastor:[3,8,6,9],than:7,welcom:0,count:1,handl:2,endpoint:[2,9,7],architectur:3,tutori:9,author:4,plai:2,authent:[3,4,6],defin:9,exampl:4,pagin:[1,6],error:2,implement:1,more:7,where:3,other:1,view:[3,9]}})
\ No newline at end of file
diff --git a/docs/build/tutorial.html b/docs/build/tutorial.html
new file mode 100644
index 0000000..b6d5526
--- /dev/null
+++ b/docs/build/tutorial.html
@@ -0,0 +1,242 @@
+
+
+
+
+
+
+
+ Tutorial building an adressebook API — Python Rest Api Framework 0.1 documentation
+
+
+
+
+
+
+
+
+
+
+
+
+
+
We also need a datastore to get a place where we can save our
+users. For instance we will use a sqlite3 database. The
+SQLiteDataStore is what we need
+
+
+
\ No newline at end of file
diff --git a/docs/source/index.rst b/docs/source/index.rst
index 7245871..d4c2c38 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -12,6 +12,7 @@ Contents:
:maxdepth: 2
introduction
+ tutorial
datastore
controller
pagination
diff --git a/docs/source/tutorial.rst b/docs/source/tutorial.rst
new file mode 100644
index 0000000..1fa00ea
--- /dev/null
+++ b/docs/source/tutorial.rst
@@ -0,0 +1,128 @@
+Tutorial building an adressebook API
+====================================
+
+First Step Building a user endpoint
+-----------------------------------
+
+For this project we need users. Users will be helpfull for our adress
+book and for our authentication process.
+
+Users will be define with at least a first name and a last name. We
+also need an unique identifier to retreive the user.
+
+Define a model
+~~~~~~~~~~~~~~
+
+.. code-block:: python
+
+ from rest_api_framework import models
+
+ class UserModel(models.Model):
+
+ fields = [models.StringField(name="first_name", required=True),
+ models.StringField(name="last_name", required=True),
+ models.PkField(name="id", required=True)
+ ]
+
+The use of required_true will ensure that a user without this field
+cannot be created
+
+Chose a DataStore
+~~~~~~~~~~~~~~~~~
+
+We also need a datastore to get a place where we can save our
+users. For instance we will use a sqlite3 database. The
+SQLiteDataStore is what we need
+
+.. code-block:: python
+
+ from rest_api_framework.datastore import SQLiteDataStore
+
+Chose a view
+~~~~~~~~~~~~
+
+We want results to be rendered as Json. We use the JsonResponse view
+for that:
+
+.. code-block:: python
+
+ from rest_api_framework.views import JsonResponse
+
+Create The user endpoint
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+To create an endpoint, we need a controller. This will manage our
+endpoint in a RESTFUL fashion.
+
+.. code-block:: python
+
+ from rest_api_framework.controllers import Controller
+
+ class UserEndPoint(Controller):
+ ressource = {
+ "ressource_name": "users",
+ "ressource": {"name": "adress_book.db", "table": "users"},
+ "model": UserModel,
+ "datastore": SQLiteDataStore
+ }
+
+ controller = {
+ "list_verbs": ["GET", "POST"],
+ "unique_verbs": ["GET", "PUT", "DElETE"]
+ }
+
+ view = {"response_class": JsonResponse}
+
+then we must run our application:
+
+.. code-block:: python
+
+ 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)
+
+Summary
+~~~~~~~
+
+So far, all of the code should look like this:
+
+.. 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
+
+
+ class UserModel(models.Model):
+
+ fields = [models.StringField(name="first_name", required=True),
+ models.StringField(name="last_name", required=True),
+ models.PkField(name="id", required=True)
+ ]
+
+
+ class UserEndPoint(Controller):
+ ressource = {
+ "ressource_name": "users",
+ "ressource": {"name": "adress_book.db", "table": "users"},
+ "model": UserModel,
+ "datastore": SQLiteDataStore
+ }
+
+ controller = {
+ "list_verbs": ["GET", "POST"],
+ "unique_verbs": ["GET", "PUT", "DElETE"]
+ }
+
+ view = {"response_class": JsonResponse}
+
+ 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)
+
+Next: :doc:`using_user_endpoint`
diff --git a/docs/source/using_user_endpoint.rst b/docs/source/using_user_endpoint.rst
new file mode 100644
index 0000000..eef864c
--- /dev/null
+++ b/docs/source/using_user_endpoint.rst
@@ -0,0 +1,72 @@
+Playing with the newly created endpoint
+=======================================
+
+First you can check that your endpoint is up
+
+.. code-block:: bash
+
+ curl -i "http://localhost:5000/users/"
+
+ HTTP/1.0 200 OK
+ Content-Type: application/json
+ Content-Length: 2
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 12:52:22 GMT
+
+Your endpoint is responding but does not have any data. Let's add
+some:
+
+Create a user
+-------------
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"John", "last_name": "Doe"}' http://localhost:5000/users/
+
+ HTTP/1.0 201 CREATED
+ Location: http://localhost:5000/users/1
+ Content-Type: application/json
+ Content-Length: 0
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:00:13 GMT
+
+Error handling
+~~~~~~~~~~~~~~
+
+If you don't provide a last_name, the API will raise a BAD REQUEST
+explaining your error:
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":"John"}' http://localhost:5000/users/
+
+ HTTP/1.0 400 BAD REQUEST
+ Content-Type: application/json
+ Content-Length: 62
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:21:10 GMT
+
+ {"error": "last_name is missing. Cannot create the ressource"}
+
+The same apply if you dont give coherent data:
+
+.. code-block:: bash
+
+ curl -i -H "Content-type: application/json" -X POST -d '{"first_name":45, "last_name": "Doe"}' http://localhost:5000/users/
+
+ HTTP/1.0 400 BAD REQUEST
+ Content-Type: application/json
+ Content-Length: 41
+ Server: Werkzeug/0.8.3 Python/2.7.2
+ Date: Mon, 14 Oct 2013 13:24:53 GMT
+ {"error": "first_name does not validate"}
+
+however, there is no duplicate check. So you can create as many "John
+Doe" you want. This could be a huge problem if your not able to
+validate uniqueness of a user. For the API, this is not a problem
+because each user is uniquely identified by his id.
+
+If you need to ensure it can be only one John Doe, you must add a
+validator on your datastore.
+
+Next: :doc:`adding_validator_datastore`
diff --git a/rest_api_framework/controllers.py b/rest_api_framework/controllers.py
index dec98c3..5cab428 100644
--- a/rest_api_framework/controllers.py
+++ b/rest_api_framework/controllers.py
@@ -47,7 +47,9 @@ def dispatch_request(self, request):
endpoint, values = adapter.match()
return getattr(self, endpoint)(request, **values)
except HTTPException, e:
- return e
+ return self.view['response_class'](
+ {"error": e.description},
+ status=e.code)
class WSGIDispatcher(DispatcherMiddleware):
diff --git a/rest_api_framework/datastore/base.py b/rest_api_framework/datastore/base.py
index 2d11ceb..13d37b0 100644
--- a/rest_api_framework/datastore/base.py
+++ b/rest_api_framework/datastore/base.py
@@ -19,9 +19,19 @@ def __init__(self, ressource_config, model, **options):
Set the ressource datastore
"""
self.ressource_config = ressource_config
- self.options = options
+ self.make_options(options)
self.model = model()
+ def make_options(self, options):
+
+ if options.get("validators", None):
+ self.validators = []
+ for elem in options["validators"]:
+ self.validators.append(elem)
+ else:
+ self.validators = None
+ self.partial = options.get("partial", None)
+
@abstractmethod
def get(self, identifier):
"""
@@ -127,8 +137,18 @@ def validate(self, data):
raise BadRequest()
for field in self.model.fields:
for validator in field.validators:
+ if not field.name in data:
+ raise BadRequest(
+ "{0} is missing. Cannot create the ressource".format(
+ field.name)
+ )
if not validator.validate(data[field.name]):
- raise BadRequest()
+ raise BadRequest("{0} does not validate".format(
+ field.name)
+ )
+ if self.validators:
+ for elem in self.validators:
+ elem.validate(self, data)
def validate_fields(self, data):
"""
@@ -144,3 +164,7 @@ def validate_fields(self, data):
for validator in field.validators:
if not validator.validate(v):
raise BadRequest()
+
+ if self.validators:
+ for elem in self.validators:
+ elem.validate(self, data)
diff --git a/rest_api_framework/datastore/simple.py b/rest_api_framework/datastore/simple.py
index 23885a9..7dca44e 100644
--- a/rest_api_framework/datastore/simple.py
+++ b/rest_api_framework/datastore/simple.py
@@ -40,8 +40,8 @@ def get_list(self, offset=0, count=None, **kwargs):
return all the objects. paginated if needed
"""
data = self.filter(**kwargs)
- if self.options.get("partial"):
- fields, kwargs = self.options["partial"].get_partials(**kwargs)
+ if self.partial:
+ fields, kwargs = self.partial.get_partials(**kwargs)
if not self.model.pk_field.name in fields:
fields.append(self.model.pk_field.name)
try:
diff --git a/rest_api_framework/datastore/sql.py b/rest_api_framework/datastore/sql.py
index 3c4fa8d..c95ce1a 100644
--- a/rest_api_framework/datastore/sql.py
+++ b/rest_api_framework/datastore/sql.py
@@ -93,15 +93,18 @@ 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
args = []
- where_query = []
- limit = kwargs.get("end", None)
+ 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 >=?")
- args.append(kwargs['start'])
+ args.append(kwargs.pop('start'))
+
if len(where_query) > 0:
- data["query"] += " WHERE"
- data["query"] += " AND".join(where_query)
+ data["query"] += " WHERE "
+ data["query"] += " AND ".join(where_query)
# a hook for ordering
data["query"] += " ORDER BY id ASC"
@@ -109,6 +112,7 @@ def paginate(self, data, **kwargs):
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)
)
@@ -118,8 +122,8 @@ def paginate(self, data, **kwargs):
return objs
def get_fields(self, **fields):
- if self.options.get("partial"):
- fields, kwargs = self.options["partial"].get_partials(**fields)
+ if self.partial:
+ fields, kwargs = self.partial.get_partials(**fields)
for field in fields:
if not field in self.model.get_fields_name():
raise BadRequest()
diff --git a/rest_api_framework/datastore/validators.py b/rest_api_framework/datastore/validators.py
new file mode 100644
index 0000000..1754391
--- /dev/null
+++ b/rest_api_framework/datastore/validators.py
@@ -0,0 +1,32 @@
+"""
+Base implementation of datastore validators. Those validators acts on
+the data they received but need a datastore to validate data
+"""
+from werkzeug.exceptions import BadRequest
+
+
+class Validator(object):
+
+ def __init__(self, *fields):
+ self.fields = fields
+ self.datastore = None
+
+ def validate(self, datastore, data):
+ raise NotImplementedError
+
+
+class UniqueTogether(Validator):
+
+ def validate(self, datastore, data):
+ """
+ Check if a record with the same fields already exists in the
+ database.
+ """
+ self.datastore = datastore
+ query_dict = {}
+ for k, v in data.iteritems():
+ if k in self.fields:
+ query_dict[k] = v
+ resp = datastore.get_list(**query_dict)
+ if len(resp) > 0:
+ raise BadRequest
diff --git a/rest_api_framework/views.py b/rest_api_framework/views.py
index 908bde8..2223507 100644
--- a/rest_api_framework/views.py
+++ b/rest_api_framework/views.py
@@ -12,7 +12,6 @@ class JsonResponse(Response):
Just like a classic Response but render json everytime
"""
def __init__(self, *args, **kwargs):
-
if args:
super(JsonResponse,
self).__init__(json.dumps(*args),
diff --git a/tests/datastore_test.py b/tests/datastore_test.py
index 54012f0..4f78723 100644
--- a/tests/datastore_test.py
+++ b/tests/datastore_test.py
@@ -33,6 +33,47 @@ def test_unfound_field(self):
class PythonListDataStoreTest(TestCase):
+ def test_validator(self):
+ from rest_api_framework.datastore.validators import UniqueTogether
+ data_list = [
+ {"name": "bob",
+ "age": a,
+ "id": a
+ } for a in range(100)
+ ]
+
+ store = PythonListDataStore(
+ data_list,
+ ApiModel,
+ validators=[UniqueTogether("age", "name")]
+ )
+
+ self.assertEqual(store.validate({"name": "bob", "age": 209}), None)
+ self.assertRaises(BadRequest,
+ store.validate,
+ {"name": "bob", "age": 20})
+
+ self.assertRaises(
+ BadRequest,
+ store.update,
+ {"name": "bob", "age": 34, "id": 34},
+ {"age": 20})
+
+ store = SQLiteDataStore(
+ {"name": "test.db", "table": "address"},
+ ApiModel,
+ validators=[UniqueTogether("age", "name")]
+ )
+
+ for i in range(100):
+ store.create({"name": "bob", "age": i+1})
+
+ self.assertEqual(store.validate({"name": "bob", "age": 209}), None)
+ self.assertRaises(BadRequest,
+ store.validate,
+ {"name": "bob", "age": 20})
+ os.remove("test.db")
+
def test_validation(self):
data_list = [