Upload
others
View
23
Download
1
Embed Size (px)
Citation preview
BuildingRESTfulPythonWebServices
TableofContents
BuildingRESTfulPythonWebServicesCreditsAbouttheAuthorAcknowledgmentsAbouttheReviewerwww.PacktPub.com
Whysubscribe?Preface
WhatthisbookcoversWhatyouneedforthisbookWhothisbookisforConventionsReaderfeedbackCustomersupport
DownloadingtheexamplecodeErrataPiracyQuestions
1.DevelopingRESTfulAPIswithDjangoDesigningaRESTfulAPItointeractwithasimpleSQLitedatabaseUnderstandingthetasksperformedbyeachHTTPmethodWorkingwithlightweightvirtualenvironmentsSettingupthevirtualenvironmentwithDjangoRESTframeworkCreatingthemodelsManagingserializationanddeserializationWritingAPIviewsMakingHTTPrequeststotheAPI
Workingwithcommand-linetools-curlandhttpieWorkingwithGUItools-Postmanandothers
TestyourknowledgeSummary
2.WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjangoUsingmodelserializerstoeliminateduplicatecodeWorkingwithwrapperstowriteAPIviewsUsingthedefaultparsingandrenderingoptionsandmovebeyondJSONBrowsingtheAPIDesigningaRESTfulAPItointeractwithacomplexPostgreSQLdatabaseUnderstandingthetasksperformedbyeachHTTPmethodDeclaringrelationshipswiththemodelsManagingserializationanddeserializationwithrelationshipsandhyperlinksCreatingclass-basedviewsandusinggenericclasses
TakingadvantageofgenericclassbasedviewsWorkingwithendpointsfortheAPICreatingandretrievingrelatedresourcesTestyourknowledgeSummary
3.ImprovingandAddingAuthenticationtoanAPIWithDjangoAddinguniqueconstraintstothemodelsUpdatingasinglefieldforaresourcewiththePATCHmethodTakingadvantageofpaginationCustomizingpaginationclassesUnderstandingauthentication,permissionsandthrottlingAddingsecurity-relateddatatothemodelsCreatingacustomizedpermissionclassforobject-levelpermissionsPersistingtheuserthatmakesarequestConfiguringpermissionpoliciesSettingadefaultvalueforanewrequiredfieldinmigrationsComposingrequestswiththenecessaryauthenticationBrowsingtheAPIwithauthenticationcredentialsTestyourknowledgeSummary
4.Throttling,Filtering,Testing,andDeployinganAPIwithDjangoUnderstandingthrottlingclassesConfiguringthrottlingpoliciesTestingthrottlingpoliciesUnderstandingfiltering,searching,andorderingclassesConfiguringfiltering,searching,andorderingforviewsTestingfiltering,searching,andorderingFiltering,searching,andorderingintheBrowsableAPISettingupunittestsWritingafirstroundofunittestsRunningunittestsandcheckingtestingcoverageImprovingtestingcoverageUnderstandingstrategiesfordeploymentsandscalabilityTestyourknowledgeSummary
5.DevelopingRESTfulAPIswithFlaskDesigningaRESTfulAPItointeractwithasimpledatasourceUnderstandingthetasksperformedbyeachHTTPmethodSettingupavirtualenvironmentwithFlaskandFlask-RESTfulDeclaringstatuscodesfortheresponsesCreatingthemodelUsingadictionaryasarepositoryConfiguringoutputfieldsWorkingwithresourcefulroutingontopofFlaskpluggableviews
ConfiguringresourceroutingandendpointsMakingHTTPrequeststotheFlaskAPI
Workingwithcommand-linetoolsâcurlandhttpieWorkingwithGUItools-Postmanandothers
TestyourknowledgeSummary
6.WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlaskDesigningaRESTfulAPItointeractwithaPostgreSQLdatabaseUnderstandingthetasksperformedbyeachHTTPmethodInstallingpackagestosimplifyourcommontasksCreatingandconfiguringthedatabaseCreatingmodelswiththeirrelationshipsCreatingschemastovalidate,serialize,anddeserializemodelsCombiningblueprintswithresourcefulroutingRegisteringtheblueprintandrunningmigrationsCreatingandretrievingrelatedresourcesTestyourknowledgeSummary
7.ImprovingandAddingAuthenticationtoanAPIwithFlaskImprovinguniqueconstraintsinthemodelsUpdatingfieldsforaresourcewiththePATCHmethodCodingagenericpaginationclassAddingpaginationfeaturesUnderstandingthestepstoaddauthenticationandpermissionsAddingausermodelCreatingaschemastovalidate,serialize,anddeserializeusersAddingauthenticationtoresourcesCreatingresourceclassestohandleusersRunningmigrationstogeneratetheusertableComposingrequestswiththenecessaryauthenticationTestyourknowledgeSummary
8.TestingandDeployinganAPIwithFlaskSettingupunittestsWritingafirstroundofunittestsRunningunittestswithnose2andcheckingtestingcoverageImprovingtestingcoverageUnderstandingstrategiesfordeploymentsandscalabilityTestyourknowledgeSummary
9.DevelopingRESTfulAPIswithTornadoDesigningaRESTfulAPItointeractwithslowsensorsandactuatorsUnderstandingthetasksperformedbyeachHTTPmethodSettingupavirtualenvironmentwithTornado
DeclaringstatuscodesfortheresponsesCreatingtheclassesthatrepresentadroneWritingrequesthandlersMappingURLpatternstorequesthandlersMakingHTTPrequeststotheTornadoAPI
Workingwithcommand-linetoolsâcurlandhttpieWorkingwithGUItools-Postmanandothers
TestyourknowledgeSummary
10.WorkingwithAsynchronousCode,Testing,andDeployinganAPIwithTornadoUnderstandingsynchronousandasynchronousexecutionRefactoringcodetotakeadvantageofasynchronousdecoratorsMappingURLpatternstoasynchronousrequesthandlersMakingHTTPrequeststotheTornadonon-blockingAPISettingupunittestsWritingafirstroundofunittestsRunningunittestswithnose2andcheckingtestingcoverageImprovingtestingcoverageOtherPythonWebframeworksforbuildingRESTfulAPIsTestyourknowledgeSummary
11.ExerciseAnswersChapter1,DevelopingRESTfulAPIswithDjangoChapter2,WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjangoChapter3,ImprovingandAddingAuthenticationtoanAPIWithDjangoChapter4,Throttling,Filtering,Testing,andDeployinganAPIwithDjangoChapter5,DevelopingRESTfulAPIswithFlaskChapter6,WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlaskChapter7,ImprovingandAddingAuthenticationtoanAPIwithFlaskChapter8,TestingandDeployinganAPIwithFlaskChapter9,DevelopingRESTfulAPIswithTornadoChapter10,WorkingwithAsynchronousCode,Testing,andDeployinganAPIwith
Tornado
BuildingRESTfulPythonWebServices
BuildingRESTfulPythonWebServicesCopyright©2016PacktPublishing
Allrightsreserved.Nopartofthisbookmaybereproduced,storedinaretrievalsystem,ortransmittedinanyformorbyanymeans,withoutthepriorwrittenpermissionofthepublisher,exceptinthecaseofbriefquotationsembeddedincriticalarticlesorreviews.
Everyefforthasbeenmadeinthepreparationofthisbooktoensuretheaccuracyoftheinformationpresented.However,theinformationcontainedinthisbookissoldwithoutwarranty,eitherexpressorimplied.Neithertheauthor,norPacktPublishing,anditsdealersanddistributorswillbeheldliableforanydamagescausedorallegedtobecauseddirectlyorindirectlybythisbook.
PacktPublishinghasendeavoredtoprovidetrademarkinformationaboutallofthecompaniesandproductsmentionedinthisbookbytheappropriateuseofcapitals.However,PacktPublishingcannotguaranteetheaccuracyofthisinformation.
Firstpublished:October2016
Productionreference:1201016
PublishedbyPacktPublishingLtd.
LiveryPlace
35LiveryStreet
Birmingham
B32PB,UK.
ISBN978-1-78646-225-1
www.packtpub.com
Credits
Author
GastónC.Hillar
CopyEditor
SnehaSingh
Reviewer
ElmerThomas
ProjectCoordinator
SheejalShah
CommissioningEditor
AaronLazar
Proofreader
SafisEditing
AcquisitionEditor
ReshmaRaman
Indexer
RekhaNair
ContentDevelopmentEditor
DivijKotian
Graphics
JasonMonteiro
TechnicalEditor
GebinGeorge
ProductionCoordinator
MelwynDsa
AbouttheAuthorGastónC.HillarisItalianandhasbeenworkingwithcomputerssincehewaseight.HebeganprogrammingwiththelegendaryTexasTI-99/4AandCommodore64homecomputersintheearly80s.HehasaBachelor'sdegreeinComputerSciencefromwhichhegraduatedwithhonors,andanMBAfromwhichhegraduatedwithanoutstandingthesis.Atpresent,GastónisanindependentITconsultantandfreelanceauthorwhoisalwayslookingfornewadventuresaroundtheworld.
HehasbeenaseniorcontributingeditoratDr.Dobb’sandhaswrittenmorethanahundredarticlesonsoftwaredevelopmenttopics.GastonwasalsoaformerMicrosoftMVPintechnicalcomputing.HehasreceivedtheprestigiousIntel®BlackBeltSoftwareDeveloperawardeighttimes.
HeisaguestbloggeratIntel®SoftwareNetwork(http://software.intel.com).Youcanreachhimatgastonhillar@hotmail.comandfollowhimonTwitterathttp://twitter.com/gastonhillar.Gastón'sblogishttp://csharpmulticore.blogspot.com.
Heliveswithhiswife,Vanesa,andhistwosons,KevinandBrandon.
AcknowledgmentsAtthetimeofwritingthisbook,IwasfortunatetoworkwithanexcellentteamatPacktPublishing,whosecontributionsvastlyimprovedthepresentationofthisbook.ReshmaRamanandAaronLazarallowedmetoprovidethemideastodevelopthisbookandIjumpedintotheexcitingprojectofteachinghowtousemanypopularwebframeworkstodevelopRESTfulWebServiceswithPython3.5.DivijKotianhelpedmerealizemyvisionforthisbookandprovidedmanysensiblesuggestionsregardingthetext,theformatandtheflow.Thereaderwillnoticehisgreatwork.ItwasgreatworkingwithDivijinanotherbook.Infact,itisthethirdbookinwhichIwasabletoworkwithReshmaandDivij.It’sbeengreatworkingwiththeminanotherprojectandIcan’twaittoworkwiththemagain.Iwouldliketothankmytechnicalreviewersandproofreaders,fortheirthoroughreviewsandinsightfulcomments.Iwasabletoincorporatesomeoftheknowledgeandwisdomtheyhavegainedintheirmanyyearsinthesoftwaredevelopmentindustry.Thisbookwaspossiblebecausetheygavevaluablefeedback.
GebinGeorgedidawonderfuljobwhenthebookmovedintotheproductionstage.Hehasmadeallthenecessaryadjustmentstogeneratethefinalversionofthebookwithanoutstandinglayout.GebinmadethebookeasytoreadinitsdifferentversionsandmadesureIwashappywiththeresults.Abooklikethisonewithsomanytables,figures,piecesofcode,commandsandsampleoutputsrequiresskilledpeoplewitheyefordetailduringallthestages.IwasfortunatetohaveGebinonboard.Iwouldliketothankmytechnicalreviewersandproofreaders,fortheirthoroughreviewsandinsightfulcomments.Iwasabletoincorporatesomeoftheknowledgeandwisdomtheyhavegainedintheirmanyyearsinthesoftwaredevelopmentindustry.Thisbookwaspossiblebecausetheygavevaluablefeedback.
IusuallystartwritingnotesaboutideasforabookwhenIspendtimeatsoftwaredevelopmentconferencesandevents.IwrotetheinitialideaforthisbookinSanFrancisco,California,atIntelDeveloperForum2015.Oneyearlater,atIntelDeveloperForum2016,IhadthechancetodiscusswithmanysoftwareengineersthebookIwasfinishingandincorporatetheirsuggestionsinthefinaldrafts.
Theentireprocessofwritingabookrequiresahugeamountoflonelyhours.Iwouldn’tbeabletowriteanentirebookwithoutdedicatingsometimetoplaysocceragainstmysonsKevinandBrandon,andmynephew,Nicolas.Ofcourse,Ineverwonamatch.However,Ididscoreafewgoals.
AbouttheReviewerElmerThomascompletedaB.S.inComputerEngineeringandaM.S.inElectricalEngineeringattheUniversityofCalifornia,Riverside.HisfocuswasonControlSystems,specificallyGPSnavigationsystems,spendingseveralyearsservingasaresearchassistant,buildingsoftwareandhardwareforselfdrivingcarsatU.C.RiversideandBerkeley,resultingin2co-publications:AidedIntegerAmbiguityResolutionAlgorithmandDataFusionviaKalmanFilter:GPS&INS.DuringthefinalyearsofhisMastersprogram,headdedafewmentors,partnersandsomebusinessskillsthroughtheTuckExecutiveProgramatDartmouthtohisrepertoireandco-foundedseveralcompanieswithvaryingdegreesofsuccessoverthenext7years.Duringthistimehehelpedhundredsofbusinessprofitwhileachievingover50awardsfromlocalandstategovernmentforserviceinthecommunity.
Whilebuildingbusinesses,ElmerservedonvariousboardstohelpfostergrowthinlocalbusinesscommunitiesinRiversideandOrangeCounty,includingtheRiversideTechnologyCEOForum,theTechBizConnection,OCTANeandTriTech.Next,hebeganservingatSendGrid,anemailAPIandServiceCompany,asoneofthefirst5employeesinanow300+employeecompanyonthevergeofgoingpublic.Servicebeganasthewebdevelopmentmanager,andthenhemovedintoaproductdevelopmentrolewhilehelpingbuildoutaqualityassuranceprogram.Afterspending2yearstravelingtoover50events,speaking,teachingandmentoringasaDeveloperEvangelistwithintheSendGridmarketingdepartment,ElmerthenservedastheHackerinResidenceonthecommunityteamatSendGrid.Inthatrolehementoredover50startups,manybelongingtoacceleratorssuchasTechstarsand500Startups,andhundredsofdevelopersthroughliveconsultinganddevelopmentofproductivitycontentandsoftware.
HecurrentlyservesastheDeveloperExperienceEngineeratSendGrid,leading,developingandmanagingSendGrid’sopensourcecommunity,whichincludesover24activeprojectsacross7programminglanguages.Theseopensourceprojectsprocesshundredsofmillionsofemailsperdayforourcustomers.HealsoservesasVicePresidentoftheCouncilfortheAdvancementofBlackEngineers,drawingfromexperienceaschapterpresidentoftheNationalSocietyofBlackEngineerswhileastudentatU.C.Riverside,supportingourmissiontoincreasethenumberofculturallyresponsibleBlackEngineerswithPhD’s,post-doctoraltrainingandprofessionalengineeringregistrations.
AsmemberoftheboardofdirectorsforOperationCode,hehelpsequipmilitaryveteransandtheirfamilieswithprogrammingknowledgethroughmentorshiptohelpveteranscreatenewcareerpathsinsoftwaredevelopment.ThroughhisvolunteerworkwiththeGirlsScoutsofSanGorgonioCouncil,ElmerfocusesonhelpingbringSTEMexperiencestogirls,specificallywithintheagegroupsbetween9and14yearsold,includinghisown11yearolddaughter,whoisnowaGirlScoutcadette.Tohelpservehislocalcommunity,heisamemberoftheboardofdirectorsofhislocalHOA.Heisconsideredasocialmediainfluencer,driving100sofmillionsofvisitstovariouswebpages.HeisknownasThinkingSeriousonvarioussocialnetworks.
Elmer'spassionsincludefamilytimewithhiswife,and2daughters,reading,writing,watchingvideos,especiallyinvirtualreality,developingsoftwareandcreatingingeneral,especiallyintheareaofpersonaldevelopmentandproductivitythroughquantificationtechniques.IwouldliketothankmywifeLindaanddaughterAudreyfortheirpatienceandquiettimeformetocompletethisreview.
Moredetailcanbefoundathisblog,ThinkingSerious.com.
www.PacktPub.comForsupportfilesanddownloadsrelatedtoyourbook,pleasevisitwww.PacktPub.com.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.
DidyouknowthatPacktofferseBookversionsofeverybookpublished,withPDFandePubfilesavailable?YoucanupgradetotheeBookversionatwww.PacktPub.comandasaprintbookcustomer,youareentitledtoadiscountontheeBookcopy.Getintouchwithusatservice@packtpub.comformoredetails.
Atwww.PacktPub.com,youcanalsoreadacollectionoffreetechnicalarticles,signupforarangeoffreenewslettersandreceiveexclusivediscountsandoffersonPacktbooksandeBooks.
https://www.packtpub.com/mapt
Getthemostin-demandsoftwareskillswithMapt.MaptgivesyoufullaccesstoallPacktbooksandvideocourses,aswellasindustry-leadingtoolstohelpyouplanyourpersonaldevelopmentandadvanceyourcareer.
Whysubscribe?FullysearchableacrosseverybookpublishedbyPacktCopyandpaste,print,andbookmarkcontentOndemandandaccessibleviaawebbrowser
PrefaceREST (RepresentationalStateTransfer)isthearchitecturalstylethatisdrivingmodernwebdevelopmentandmobileapps.Infact,developingandinteractingwithRESTfulWebServicesisarequiredskillinanymodernsoftwaredevelopmentjob.Sometimes,youhavetointeractwithanexistingAPIandinothercases,youhavetodesignaRESTfulAPIfromscratchandmakeitworkwithJSON(JavaScriptObjectNotation).
Pythonisoneofthemostpopularprogramminglanguages.Python3.5isthemostmodernversionofPython.Itisopensource,multiplatform,andyoucanuseittodevelopanykindofapplication,fromwebsitestoextremelycomplexscientificcomputingapplications.ThereisalwaysaPythonpackagethatmakesthingseasierforyoutoavoidreinventingthewheelandsolvetheproblemsfaster.ThemostimportantandpopularCloudcomputingprovidersmakeiteasytoworkwithPythonanditsrelatedWebframeworks.Thus,PythonisanidealchoicefordevelopingRESTfulWebServices.ThebookcoversallthethingsyouneedtoknowtoselectthemostappropriatePythonWebframeworkanddevelopaRESTfulAPIfromscratch.
YouwillworkwiththethreemostpopularPythonwebframeworksthatmakeiteasytodevelopRESTfulWebServices:Django,Flask,andTornado.Eachwebframeworkhasitsadvantagesandtradeoffs.YouwillworkwithexamplesthatrepresentappropriatecasesforeachoftheseWebframeworks,incombinationwithadditionalPythonpackagesthatwillsimplifythemostcommontasks.Youwilllearntousedifferenttoolstotestanddevelophigh-quality,consistentandscalableRESTfulWebServices.Youwillalsotakeadvantageofobject-orientedprogramming,alsoknownasOOP,tomaximizecodereuseandminimizemaintenancecosts.
YouwillalwayswriteunittestsandimprovetestcoverageforalloftheRESTfulWebServicesthatyouwilldevelopthroughoutthebook.Youwon’tjustrunthesamplecodebutyouwillalsomakesurethatyouwritetestsforyourRESTfulAPI.
ThisbookwillallowyoutolearnhowtotakeadvantageofmanypackagesthatwillsimplifythemostcommontasksrelatedtoRESTfulWebServices.YouwillbeabletostartcreatingyourownRESTfulAPIsforanydomaininanyofthecoveredWebframeworksinPython3.5orgreater.
WhatthisbookcoversChapter1,DevelopingRESTfulAPIswithDjango,inthischapterwewillstartworkingwithDjangoandDjangoRESTFramework,andwewillcreateaRESTfulWebAPIthatperformsCRUD(Create,Read,UpdateandDelete)operationsonasimpleSQLitedatabase.
Chapter2,WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjango,inthischapterwewillexpandthecapabilitiesoftheRESTfulAPIthatwestartedinthepreviouschapter.WewillchangetheORMsettingstoworkwithamorepowerfulPostgreSQLdatabaseandwewilltakeadvantageofadvancedfeaturesincludedinDjangoRESTFrameworkthatallowustoreduceboilerplatecodeforcomplexAPIs,suchasclassbasedviews.
Chapter3,ImprovingandAddingAuthenticationtoanAPIwithDjango,inthischapterwewillimprovetheRESTfulAPIthatwestartedinthepreviouschapter.Wewilladduniqueconstraintstothemodelandupdatethedatabase.WewillmakeiteasytoupdatesinglefieldswiththePATCHmethodandwewilltakeadvantageofpagination.Wewillstartworkingwithauthentication,permissionsandthrottling.
Chapter4,Throttling,Filtering,TestingandDeployinganAPIwithDjango,inthischapterwewilltakeadvantageofmanyfeaturesincludedinDjangoRESTFrameworktodefinethrottlingpolicies.Wewillusefiltering,searchingandorderingclassestomakeiteasytoconfigurefilters,searchqueriesanddesiredorderfortheresultsinHTTPrequests.WewillusethebrowsableAPIfeaturetotestthesenewfeaturesincludedinourAPI.Wewillwriteafirstroundofunittests,measuretestcoverageandthenwriteadditionalunitteststoimprovetestcoverage.Finally,wewilllearnmanyconsiderationsfordeploymentandscalability.
Chapter5,DevelopingRESTfulAPIswithFlask,inthischapterwewillstartworkingwithFlaskanditsFlask-RESTfulextension.WewillcreateaRESTfulWebAPIthatperformsCRUDoperationsonasimplelist.
Chapter6,WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlask,inthischapterwewillexpandthecapabilitiesoftheRESTfulAPIthatwestartedinthepreviouschapter.WewilluseSQLAlchemyasourORMtoworkwithaPostgreSQLdatabaseandwewilltakeadvantageofadvancedfeaturesincludedinFlaskandFlask-RESTfulthatwillallowustoeasilyorganizecodeforcomplexAPIs,suchasmodelsandblueprints.
Chapter7,ImprovingandAddingAuthenticationtoanAPIwithFlask,inthischapterwewillimprovetheRESTfulAPIinmanyways.Wewilladduserfriendlyerrormessageswhenresourcesaren’tunique.WewilltesthowtoupdatesingleormultiplefieldswiththePATCHmethodandwewillcreateourowngenericpaginationclass.Then,wewillstartworkingwithauthenticationandpermissions.Wewilladdedausermodelandwewillupdatethedatabase.WewillmakemanychangesinthedifferentpiecesofcodetoachieveaspecificsecuritygoalandwewilltakeadvantageofFlask-HTTPAuthandpasslibtouseHTTPauthenticationinourAPI.
Chapter8,TestingandDeployinganAPIwithFlask,inthischapterwewillsetupatestingenvironment.Wewillinstallnose2tomakeiteasytodiscoverandexecuteunittestsandwewillcreateanewdatabasetobeusedfortesting.Wewillwriteafirstroundofunittests,measuretestcoverageandthenwriteadditionalunitteststoimprovetestcoverage.Finally,wewilllearnmanyconsiderationsfordeploymentandscalability.
Chapter9,DevelopingRESTfulAPIswithTornado,wewillworkwithTornadotocreateaRESTfulWebAPI.WewilldesignaRESTfulAPItointeractwithslowsensorsandactuators.WewilldefinedtherequirementsforourAPIandwewillunderstandthetasksperformedbyeachHTTPmethod.WewillcreatetheclassesthatrepresentadroneandwritecodetosimulateslowI/OoperationsthatarecalledforeachHTTPrequestmethod.WewillwriteclassesthatrepresentrequesthandlersandprocessthedifferentHTTPrequestsandconfiguretheURLpatternstorouteURLstorequesthandlersandtheirmethods.
Chapter10,WorkingwithAsynchronousCode,Testing,andDeployinganAPIwithTornado,inthischapterwewillunderstandthedifferencebetweensynchronousandasynchronousexecution.WewillcreateanewversionoftheRESTfulAPIthattakesadvantageofthenon-blockingfeaturesinTornadocombinedwithasynchronousexecution.WewillimprovescalabilityforourexistingAPIandwewillmakeitpossibletostartexecutingotherrequestswhilewaitingfortheslowI/Ooperationswithsensorsandactuators.Then,wewillsetupatestingenvironment.Wewillinstallnose2tomakeiteasytodiscoverandexecuteunittests.Wewillwroteafirstroundofunittests,measuretestcoverageandthenwriteadditionalunitteststoimprovetestcoverage.Wewillcreateallthenecessaryteststohaveacompletecoverageofallthelinesofcode.
WhatyouneedforthisbookInordertoworkwiththedifferentsamplesforPython3.5.x,youwillneedanycomputerwithanIntelCorei3orhigherCPUandatleast4GBRAM.Youcanworkwithanyofthefollowingoperatingsystems:
Windows7orgreater(Windows8,Windows8.1orWindows10)macOSMountainLionorgreaterAnyLinuxversioncapableofrunningPython3.5.xandanymodernbrowserwithJavaScriptsupport
YouwillneedPython3.5orgreaterinstalledonyourcomputer.
WhothisbookisforThisbookisforwebdeveloperswhohaveworkingknowledgeofPythonandwouldliketobuildamazingwebservicesbytakingadvantageofthevariousframeworksofPython.YoushouldhavesomeknowledgeofRESTfulAPIs.
ConventionsInthisbook,youwillfindanumberoftextstylesthatdistinguishbetweendifferentkindsofinformation.Herearesomeexamplesofthesestylesandanexplanationoftheirmeaning.
Codewordsintext,databasetablenames,foldernames,filenames,fileextensions,pathnames,dummyURLs,userinput,andTwitterhandlesareshownasfollows:"Ifnogamematchesthespecifiedidorprimarykey,theserverwillreturnjusta404NotFoundstatus."
Ablockofcodeissetasfollows:
fromdjango.appsimportAppConfig
classGamesConfig(AppConfig):
name='games'
Anycommand-lineinputoroutputiswrittenasfollows:
python3-mvenv~/PythonREST/Django01
Note
Warningsorimportantnotesappearinaboxlikethis.
Tip
Tipsandtricksappearlikethis.
ReaderfeedbackFeedbackfromourreadersisalwayswelcome.Letusknowwhatyouthinkaboutthisbook-whatyoulikedordisliked.Readerfeedbackisimportantforusasithelpsusdeveloptitlesthatyouwillreallygetthemostoutof.Tosendusgeneralfeedback,[email protected],andmentionthebook'stitleinthesubjectofyourmessage.Ifthereisatopicthatyouhaveexpertiseinandyouareinterestedineitherwritingorcontributingtoabook,seeourauthorguideatwww.packtpub.com/authors.
CustomersupportNowthatyouaretheproudownerofaPacktbook,wehaveanumberofthingstohelpyoutogetthemostfromyourpurchase.
DownloadingtheexamplecodeYoucandownloadtheexamplecodefilesforthisbookfromyouraccountathttp://www.packtpub.com.Ifyoupurchasedthisbookelsewhere,youcanvisithttp://www.packtpub.com/supportandregistertohavethefilese-maileddirectlytoyou.
Youcandownloadthecodefilesbyfollowingthesesteps:
1. Loginorregistertoourwebsiteusingyoure-mailaddressandpassword.2. HoverthemousepointerontheSUPPORT tabatthetop.3. ClickonCodeDownloads&Errata.4. EnterthenameofthebookintheSearchbox.5. Selectthebookforwhichyou'relookingtodownloadthecodefiles.6. Choosefromthedrop-downmenuwhereyoupurchasedthisbookfrom.7. ClickonCodeDownload.
Oncethefileisdownloaded,pleasemakesurethatyouunziporextractthefolderusingthelatestversionof:
WinRAR/7-ZipforWindowsZipeg/iZip/UnRarXforMac7-Zip/PeaZipforLinux
ThecodebundleforthebookisalsohostedonGitHubathttps://github.com/PacktPublishing/Building-RESTful-Python-Web-Services.Wealsohaveothercodebundlesfromourrichcatalogofbooksandvideosavailableathttps://github.com/PacktPublishing/.Checkthemout!
ErrataAlthoughwehavetakeneverycaretoensuretheaccuracyofourcontent,mistakesdohappen.Ifyoufindamistakeinoneofourbooks-maybeamistakeinthetextorthecode-wewouldbegratefulifyoucouldreportthistous.Bydoingso,youcansaveotherreadersfromfrustrationandhelpusimprovesubsequentversionsofthisbook.Ifyoufindanyerrata,pleasereportthembyvisitinghttp://www.packtpub.com/submit-errata,selectingyourbook,clickingontheErrataSubmissionFormlink,andenteringthedetailsofyourerrata.Onceyourerrataareverified,yoursubmissionwillbeacceptedandtheerratawillbeuploadedtoourwebsiteoraddedtoanylistofexistingerrataundertheErratasectionofthattitle.
Toviewthepreviouslysubmittederrata,gotohttps://www.packtpub.com/books/content/supportandenterthenameofthebookinthesearchfield.TherequiredinformationwillappearundertheErratasection.
PiracyPiracyofcopyrightedmaterialontheInternetisanongoingproblemacrossallmedia.AtPackt,wetaketheprotectionofourcopyrightandlicensesveryseriously.IfyoucomeacrossanyillegalcopiesofourworksinanyformontheInternet,pleaseprovideuswiththelocationaddressorwebsitenameimmediatelysothatwecanpursuearemedy.
Pleasecontactusatcopyright@packtpub.comwithalinktothesuspectedpiratedmaterial.
Weappreciateyourhelpinprotectingourauthorsandourabilitytobringyouvaluablecontent.
QuestionsIfyouhaveaproblemwithanyaspectofthisbook,[email protected],andwewilldoourbesttoaddresstheproblem.
Chapter1.DevelopingRESTfulAPIswithDjangoInthischapter,wewillstartourjourneytowardsRESTfulWebAPIswithPythonandfourdifferentWebframeworks.Pythonisoneofthemostpopularandversatileprogramminglanguages.TherearethousandsofPythonpackages,whichallowyoutoextendPythoncapabilitiestoanykindofdomainyoucanimagine.WecanworkwithmanydifferentWebframeworksandpackagestoeasilybuildsimpleandcomplexRESTfulWebAPIswithPython,andwecanalsocombinetheseframeworkswithotherPythonpackages.
WecanleverageourexistingknowledgeofPythonanditspackagestocodethedifferentpiecesofourRESTfulWebAPIsandtheirecosystem.Wecanusetheobject-orientedfeaturestocreatecodethatiseasiertomaintain,understand,andreuse.Wecanuseallthepackagesthatwealreadyknowtointeractwithdatabases,Webservices,anddifferentAPIs.PythonmakesiteasyforustocreateRESTfulWebAPIs.Wedon'tneedtolearnanotherprogramminglanguage;wecanusetheonewealreadyknowandlove.
Inthischapter,wewillstartworkingwithDjangoandDjangoRESTFramework,andwewillcreateaRESTfulWebAPIthatperformsCRUD(Create,Read,Update,andDelete)operationsonasimpleSQLitedatabase.Wewill:
DesignaRESTfulAPItointeractwithasimpleSQLitedatabaseUnderstandthetasksperformedbyeachHTTPmethodSetupthevirtualenvironmentwithDjangoRESTframeworkCreatethedatabasemodelsManageserializationanddeserializationofdataWriteAPIviewsMakeHTTPrequeststotheAPIwithcommand-linetoolsWorkwithGUItoolstocomposeandsendHTTPrequests
DesigningaRESTfulAPItointeractwithasimpleSQLitedatabaseImaginethatwehavetostartworkingonamobileAppthathastointeractwithaRESTfulAPItoperformCRUDoperationswithgames.Wedon'twanttospendtimechoosingandconfiguringthemostappropriateORM(Object-RelationalMapping);wejustwanttofinishtheRESTfulAPIassoonaspossibletostartinteractingwithitviaourmobileApp.Wereallywantthegamestopersistinadatabasebutwedon'tneedittobeproduction-ready,andtherefore,wecanusethesimplestpossiblerelationaldatabase,aslongaswedon'thavetospendtimemakingcomplexinstallationsorconfigurations.
DjangoRESTframework,alsoknownasDRF,willallowustoeasilyaccomplishthistaskandstartmakingHTTPrequeststoourfirstversionofourRESTfulWebService.Inthiscase,wewillworkwithaverysimpleSQLitedatabase,thedefaultdatabaseforanewDjangoRESTframeworkproject.
First,wemustspecifytherequirementsforourmainresource:agame.Weneedthefollowingattributesorfieldsforagame:
AnintegeridentifierAnameortitleAreleasedateAgamecategorydescription,suchas3DRPGand2Dmobilearcade.Aboolvalueindicatingwhetherthegamewasplayedatleastoncebyaplayerornot
Inaddition,wewantourdatabasetosaveatimestampwiththedateandtimeinwhichthegamewasinsertedinthedatabase.
ThefollowingtableshowstheHTTPverbs,thescope,andthesemanticsforthemethodsthatourfirstversionoftheAPImustsupport.EachmethodiscomposedbyanHTTPverbandascopeandallthemethodshaveawelldefinedmeaningforallgamesandcollections.
HTTPverb Scope Semantics
GETCollectionofgames
Retrieveallthestoredgamesinthecollection,sortedbytheirnameinascendingorder
GET Game Retrieveasinglegame
Collectionof
POST games Createanewgameinthecollection
PUT Game Updateanexistinggame
DELETE Game Deleteanexistinggame
Tip
InaRESTfulAPI,eachresourcehasitsownuniqueURL.InourAPI,eachgamehasitsownuniqueURL.
UnderstandingthetasksperformedbyeachHTTPmethodIntheprecedingtable,theGETHTTPverbappearstwicebutwithtwodifferentscopes.ThefirstrowshowsaGETHTTPverbappliedtoacollectionofgames(collectionofresources)andthesecondrowshowsaGETHTTPverbappliedtoagame(asingleresource).
Let'sconsiderthathttp://localhost:8000/games/istheURLforthecollectionofgames.Ifweaddanumberandaslash(/)totheprecedingURL,weidentifyaspecificgamewhoseidorprimarykeyisequaltothespecifiednumericvalue.Forexample,http://localhost:8000/games/12/identifiesthegamewhoseidorprimarykeyisequalto12.
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(POST)andrequestURL(http://localhost:8000/games/)tocreateanewgame.Inaddition,wehavetoprovidetheJSON(JavaScriptObjectNotation)key-valuepairswiththefieldnamesandthevaluestocreatethenewgame.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefields,makesurethatitisavalidgameandpersistitinthedatabase.
Theserverwillinsertanewrowwiththenewgameintheappropriatetableanditwillreturna201CreatedstatuscodeandaJSONbodywiththerecentlyaddedgameserializedtoJSON,includingtheassignedidorprimarykeythatwasautomaticallygeneratedbythedatabaseandassignedtothegameobject.
POSThttp://localhost:8000/games/
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:8000/games/{id}/)toretrievethegamewhoseidorprimarykeymatchesthespecifiednumericvalueintheplacewhere{id}iswritten.
Forexample,ifweusetherequestURLhttp://localhost:8000/games/50/,theserverwillretrievethegamewhoseidorprimarykeymatches50.
Asaresultoftherequest,theserverwillretrieveagamewiththespecifiedidorprimarykeyfromthedatabaseandcreatetheappropriategameobjectinPython.Ifagameisfound,theserverwillserializethegameobjectintoJSONandreturna200OKstatuscodeandaJSONbodywiththeserializedgameobject.Ifnogamematchesthespecifiedidorprimarykey,theserverwillreturnjusta404NotFoundstatus:
GEThttp://localhost:8000/games/{id}/
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(PUT)andrequestURL(http://localhost:8000/games/{id}/)toretrievethegamewhoseidorprimarykeymatchesthespecifiednumericvalueintheplacewhere{id}iswrittenand
replaceitwithagamecreatedwiththeprovideddata.Inaddition,wehavetoprovidetheJSONkey-valuepairswiththefieldnamesandthevaluestocreatethenewgamethatwillreplacetheexistingone.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefields,makesurethatitisavalidgameandreplacetheonethatmatchesthespecifiedidorprimarykeywiththenewoneinthedatabase.Theidorprimarykeyforthegamewillbethesameaftertheupdateoperation.Theserverwillupdatetheexistingrowintheappropriatetableanditwillreturna200OKstatuscodeandaJSONbodywiththerecentlyupdatedgameserializedtoJSON.Ifwedon'tprovideallthenecessarydataforthenewgame,theserverwillreturna400BadRequeststatuscode.Iftheserverdoesn'tfindagamewiththespecifiedid,theserverwillreturnjusta404NotFoundstatus.
PUThttp://localhost:8000/games/{id}/
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(DELETE)andrequestURL(http://localhost:8000/games/{id}/)toremovethegamewhoseidorprimarykeymatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Forexample,ifweusetherequestURLhttp://localhost:8000/games/20/,theserverwilldeletethegamewhoseidorprimarykeymatches20.Asaresultoftherequest,theserverwillretrieveagamewiththespecifiedidorprimarykeyfromthedatabaseandcreatetheappropriategameobjectinPython.Ifagameisfound,theserverwillrequesttheORMtodeletethegamerowassociatedwiththisgameobjectandtheserverwillreturna204NoContentstatuscode.Ifnogamematchesthespecifiedidorprimarykey,theserverwillreturnjusta404NotFoundstatus.
DELETEhttp://localhost:8000/games/{id}/
WorkingwithlightweightvirtualenvironmentsThroughoutthisbook,wewillbeworkingwithdifferentframeworksandlibraries,andtherefore,itisconvenienttoworkwithvirtualenvironments.WewillworkwiththelightweightvirtualenvironmentsintroducedinPython3.3andimprovedinPython3.4.However,youcanalsochoosetousethepopularvirtualenv(https://pypi.python.org/pypi/virtualenv)third-partyvirtualenvironmentbuilderorthevirtualenvironmentoptionsprovidedbyyourPythonIDE.
Youjusthavetomakesurethatyouactivateyourvirtualenvironmentwiththeappropriatemechanismwhenitisnecessarytodoso,insteadoffollowingthestepexplainedtoactivatethevirtualenvironmentgeneratedwiththevenvmoduleintegratedinPython.YoucanreadmoreinformationaboutPEP405PythonVirtualEnvironmentthatintroducedthevenvmoduleathttps://www.python.org/dev/peps/pep-0405.
Tip
EachvirtualenvironmentwecreatewithvenvisanisolatedenvironmentanditwillhaveitsownindependentsetofinstalledPythonpackagesinitssitedirectories.WhenwecreateavirtualenvironmentwithvenvinPython3.4andgreater,pipisincludedinthenewvirtualenvironment.InPython3.3,itwasnecessarytomanuallyinstallpipaftercreatingthevirtualenvironment.NoticethattheinstructionsprovidedarecompatiblewithPython3.4orgreater,includingPython3.5.x.ThefollowingcommandsassumethatyouhavePython3.5.xinstalledonmacOS,Linux,orWindows.
First,wehavetoselectthetargetfolderordirectoryforourvirtualenvironment.ThefollowingisthepathwewilluseintheexampleformacOSandLinux.ThetargetfolderforthevirtualenvironmentwillbethePythonREST/Djangofolderwithinourhomedirectory.Forexample,ifourhomedirectoryinmacOSorLinuxis/Users/gaston,thevirtualenvironmentwillbecreatedwithin/Users/gaston/PythonREST/Django.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand.
~/PythonREST/Django
ThefollowingisthepathwewilluseintheexampleforWindows.ThetargetfolderforthevirtualenvironmentwillbethePythonREST/Djangofolderwithinouruserprofilefolder.Forexample,ifouruserprofilefolderisC:\Users\Gaston,thevirtualenvironmentwillbecreatedwithinC:\Users\gaston\PythonREST\Django.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand.
%USERPROFILE%\PythonREST\Django
Now,wehavetousethe-moptionfollowedbythevenvmodulenameandthedesiredpathtomakePythonrunthismoduleasascriptandcreateavirtualenvironmentinthespecifiedpath.Theinstructionsaredifferentdependingontheplatforminwhichwearecreatingthevirtual
environment.
OpenaTerminalinmacOSorLinuxandexecutethefollowingcommandtocreateavirtualenvironment:
python3-mvenv~/PythonREST/Django01
InWindows,executethefollowingcommandtocreateavirtualenvironment:
python-mvenv%USERPROFILE%\PythonREST\Django01
Theprecedingcommanddoesn'tproduceanyoutput.Thescriptcreatedthespecifiedtargetfolderandinstalledpipbyinvokingensurepipbecausewedidn'tspecifythe--without-pipoption.ThespecifiedtargetfolderhasanewdirectorytreethatcontainsPythonexecutablefilesandotherfilesthatindicatethatitisavirtualenvironment.
Thepyenv.cfgconfigurationfilespecifiesdifferentoptionsforthevirtualenvironmentanditsexistenceisanindicatorthatweareintherootfolderforavirtualenvironment.InOSandLinux,thefolderwillhavethefollowingmainsub-folders—bin,include,lib,lib/python3.5andlib/python3.5/site-packages.InWindows,thefolderwillhavethefollowingmainsub-folders—Include,Lib,Lib\site-packages,andScripts.ThedirectorytreesforthevirtualenvironmentineachplatformarethesameasthelayoutofthePythoninstallationintheseplatforms.ThefollowingscreenshotshowsthefoldersandfilesinthedirectorytreesgeneratedfortheDjango01virtualenvironmentinmacOS:
Thefollowingscreenshotshowsthemainfoldersinthedirectorytreesgeneratedforthe
virtualenvironmentsinWindows:
Tip
Afterweactivatethevirtualenvironment,wewillinstallthird-partypackagesintothevirtualenvironmentandthemoduleswillbelocatedwithinthelib/python3.5/site-packagesorLib\site-packagesfolder,basedontheplatform.TheexecutableswillbecopiedinthebinorScriptsfolder,basedontheplatform.Thepackagesweinstallwon'tmakechangestoothervirtualenvironmentsorourbasePythonenvironment.
Nowthatwehavecreatedavirtualenvironment,wewillrunaplatform-specificscripttoactivateit.Afterweactivatethevirtualenvironment,wewillinstallpackagesthatwillonlybeavailableinthisvirtualenvironment.
RunthefollowingcommandintheterminalinmacOSorLinux.Notethattheresultsofthiscommandwillbeaccurateifyoudon'tstartadifferentshellthanthedefaultshellintheterminalsession.Incaseyouhavedoubts,checkyourterminalconfigurationandpreferences.
echo$SHELL
ThecommandwilldisplaythenameoftheshellyouareusingintheTerminal.InmacOS,thedefaultis/bin/bashandthismeansyouareworkingwiththebashshell.Dependingontheshell,youmustrunadifferentcommandtoactivatethevirtualenvironmentinOSorLinux.
IfyourTerminalisconfiguredtousethebashshellinmacOSorLinux,runthefollowingcommandtoactivatethevirtualenvironment.Thecommandalsoworksforthezshshell:
source~/PythonREST/Django01/bin/activate
IfyourTerminalisconfiguredtouseeitherthecshortcshshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Django01/bin/activate.csh
IfyourTerminalisconfiguredtouseeitherthefishshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Django01/bin/activate.fish
InWindows,youcanruneitherabatchfileinthecommandpromptoraWindowsPowerShellscripttoactivatethevirtualenvironment.Ifyoupreferthecommandprompt,runthefollowingcommandintheWindowscommandlinetoactivatethevirtualenvironment:
%USERPROFILE%\PythonREST\Django01\Scripts\activate.bat
IfyouprefertheWindowsPowerShell,launchitandrunthefollowingcommandstoactivatethevirtualenvironment.However,noticethatyoushouldhavescriptsexecutionenabledinWindowsPowerShelltobeabletorunthescript:
cd$env:USERPROFILE
PythonREST\Django01\Scripts\Activate.ps1
Afteryouactivatethevirtualenvironment,thecommandpromptwilldisplaythevirtualenvironmentrootfoldernameenclosedinparenthesisasaprefixofthedefaultprompttoremindusthatweareworkinginthevirtualenvironment.Inthiscase,wewillsee(Django01)asaprefixforthecommandpromptbecausetherootfolderfortheactivatedvirtualenvironmentisDjango01.
ThefollowingscreenshotshowsthevirtualenvironmentactivatedinamacOSElCapitanterminalwithabashshell,afterexecutingthepreviouslyshowncommands:
Aswecanseeintheprecedingscreenshot,thepromptchangedfromGastons-MacBook-Pro:~gaston$to(Django01)Gastons-MacBook-Pro:~gaston$aftertheactivationofthevirtualenvironment.
ThefollowingscreenshotshowsthevirtualenvironmentactivatedinaWindows10CommandPrompt,afterexecutingthepreviouslyshowncommands:
Aswecannoticefromtheprecedingscreenshot,thepromptchangedfromC:\Users\gaston\AppData\Local\Programs\Python\Python35to(Django01)C:\Users\gaston\AppData\Local\Programs\Python\Python35aftertheactivationofthevirtualenvironment.
Tip
Itisextremelyeasytodeactivateavirtualenvironmentgeneratedwiththepreviouslyexplainedprocess.InmacOSorLinux,justtypedeactivateandpressEnter.InaWindowscommandprompt,youhavetorunthedeactivate.batbatchfileincludedintheScriptsfolder(%USERPROFILE%\PythonREST\Django01\Scripts\deactivate.batinourexample).InWindowsPowerShell,youhavetoruntheDeactivate.ps1scriptintheScriptsfolder.Thedeactivationwillremoveallthechangesmadeintheenvironmentvariables.
SettingupthevirtualenvironmentwithDjangoRESTframeworkWehavecreatedandactivatedavirtualenvironment.ItistimetorunmanycommandsthatwillbethesameforeithermacOS,LinuxorWindows.Now,wemustrunthefollowingcommandtoinstalltheDjangoWebframework:
pipinstalldjango
Thelastlinesoftheoutputwillindicatethatthedjangopackagehasbeensuccessfullyinstalled.Takeintoaccountthatyoumayalsoseeanoticetoupgradepip.
Collectingdjango
Installingcollectedpackages:django
Successfullyinstalleddjango-1.10
NowthatwehaveinstalledDjangoWebframework,wecaninstallDjangoRESTframework.Wejustneedtorunthefollowingcommandtoinstallthispackage:
pipinstalldjangorestframework
Thelastlinesfortheoutputwillindicatethatthedjangorestframeworkpackagehasbeensuccessfullyinstalled:
Collectingdjangorestframework
Installingcollectedpackages:djangorestframework
Successfullyinstalleddjangorestframework-3.3.3
Gototherootfolderforthevirtualenvironment-Django01.InmacOSorLinux,enterthefollowingcommand:
cd~/PythonREST/Django01
InWindows,enterthefollowingcommand:
cd/d%USERPROFILE%\PythonREST\Django01
RunthefollowingcommandtocreateanewDjangoprojectnamedgamesapi.Thecommandwon'tproduceanyoutput:
django-admin.pystartprojectgamesapi
Thepreviouscommandcreatedagamesapifolderwithothersub-foldersandPythonfiles.Now,gototherecentlycreatedgamesapifolder.Justexecutethefollowingcommand:
cdgamesapi
Then,runthefollowingcommandtocreateanewDjangoappnamedgameswithinthe
gamesapiDjangoproject.Thecommandwon'tproduceanyoutput:
pythonmanage.pystartappgames
Thepreviouscommandcreatedanewgamesapi/gamessub-folder,withthefollowingfiles:
__init__.py
admin.py
apps.py
models.py
tests.py
views.py
Inaddition,thegamesapi/gamesfolderwillhaveamigrationssub-folderwithan__init__.pyPythonscript.Thefollowingdiagramshowsthefoldersandfilesinthedirectorytreesstartingatthegamesapifolder:
Let'scheckthePythoncodeintheapps.pyfilewithinthegamesapi/gamesfolder.The
followinglinesshowsthecodeforthisfile:
fromdjango.appsimportAppConfig
classGamesConfig(AppConfig):
name='games'
ThecodedeclarestheGamesConfigclassasasubclassofthedjango.apps.AppConfigclassthatrepresentsaDjangoapplicationanditsconfiguration.TheGamesConfigclassjustdefinesthenameclassattributeandsetsitsvalueto'games'.Wehavetoaddgames.apps.GamesConfigasoneoftheinstalledappsinthegamesapi/settings.pyfilethatconfiguressettingsforthegamesapiDjangoproject.Webuilttheprecedingstringasfollows-appname+.apps.+classname,whichis,games+.apps.+GamesConfig.Inaddition,wehavetoaddtherest_frameworkapptomakeitpossibleforustouseDjangoRESTFramework.
Thegamesapi/settings.pyfileisaPythonmodulewithmodule-levelvariablesthatdefinetheconfigurationofDjangoforthegamesapiproject.WewillmakesomechangestothisDjangosettingsfile.Openthegamesapi/settings.pyfileandlocatethefollowinglinesthatspecifythestringslistthatdeclarestheinstalledapps:
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
]
AddthefollowingtwostringstotheINSTALLED_APPSstringslistandsavethechangestothegamesapi/settings.pyfile:
'rest_framework'
'games.apps.GamesConfig'
ThefollowinglinesshowthenewcodethatdeclarestheINSTALLED_APPSstringslistwiththeaddedlineshighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#DjangoRESTFramework
'rest_framework',
#Gamesapplication
'games.apps.GamesConfig',
]
Thisway,wehaveaddedDjangoRESTFrameworkandthegamesapplicationtoourinitialDjangoprojectnamedgamesapi.
CreatingthemodelsNow,wewillcreateasimpleGamemodelthatwewillusetorepresentandpersistgames.Openthegames/models.pyfile.Thefollowinglinesshowtheinitialcodeforthisfile,withjustoneimportstatementandacommentthatindicatesweshouldcreatethemodels:
fromdjango.dbimportmodels
#Createyourmodelshere.
ThefollowinglinesshowthenewcodethatcreatesaGameclass,specifically,aGamemodelinthegames/models.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
fromdjango.dbimportmodels
classGame(models.Model):
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=200,blank=True,default='')
release_date=models.DateTimeField()
game_category=models.CharField(max_length=200,blank=True,default='')
played=models.BooleanField(default=False)
classMeta:
ordering=('name',)
TheGameclassisasubclassofthedjango.db.models.Modelclass.Eachdefinedattributerepresentsadatabasecolumnorfield.Djangoautomaticallyaddsanauto-incrementintegerprimarykeycolumnnamedidwhenitcreatesthedatabasetablerelatedtothemodel.However,themodelmapstheunderlyingidcolumninanattributenamedpkforthemodel.Wespecifiedthefieldtypes,maximumlengthsanddefaultsformanyattributes.TheclassdeclaresaMetainnerclassthatdeclaresaorderingattributeandsetsitsvaluetoatupleofstringwhosefirstvalueisthe'name'string,indicatingthat,bydefault,wewanttheresultsorderedbythenameattributeinascendingorder.
Then,itisnecessarytocreatetheinitialmigrationforthenewGamemodelwerecentlycoded.WejustneedtorunthefollowingPythonscriptsandwewillalsosynchronizethedatabaseforthefirsttime.Bydefault,DjangousesanSQLitedatabase.Inthisexample,wewillbeworkingwiththisdefaultconfiguration:
pythonmanage.pymakemigrationsgames
Thefollowinglinesshowtheoutputgeneratedafterrunningtheprecedingcommand.
Migrationsfor'games':
0001_initial.py:
-CreatemodelGame
Theoutputindicatesthatthegamesapi/games/migrations/0001_initial.pyfileincludesthecodetocreatetheGamemodel.ThefollowinglinesshowthecodeforthisfilethatwasautomaticallygeneratedbyDjango.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
#-*-coding:utf-8-*-
#GeneratedbyDjango1.9.6on2016-05-1721:19
from__future__importunicode_literals
fromdjango.dbimportmigrations,models
classMigration(migrations.Migration):
initial=True
dependencies=[
]
operations=[
migrations.CreateModel(
name='Game',
fields=[
('id',models.AutoField(auto_created=True,primary_key=True,
serialize=False,verbose_name='ID')),
('created',models.DateTimeField(auto_now_add=True)),
('name',models.CharField(blank=True,default='',
max_length=200)),
('release_date',models.DateTimeField()),
('game_category',models.CharField(blank=True,default='',
max_length=200)),
('played',models.BooleanField(default=False)),
],
options={
'ordering':('name',),
},
),
]
Thecodedefinesasubclassofthedjango.db.migrations.MigrationclassnamedMigrationthatdefinesanoperationthatcreatestheGamemodel'stable.Now,runthefollowingpythonscripttoapplyallthegeneratedmigrations:
pythonmanage.pymigrate
Thefollowinglinesshowtheoutputgeneratedafterrunningtheprecedingcommand:
Operationstoperform:
Applyallmigrations:sessions,games,contenttypes,admin,auth
Runningmigrations:
Renderingmodelstates...DONE
Applyingcontenttypes.0001_initial...OK
Applyingauth.0001_initial...OK
Applyingadmin.0001_initial...OK
Applyingadmin.0002_logentry_remove_auto_add...OK
Applyingcontenttypes.0002_remove_content_type_name...OK
Applyingauth.0002_alter_permission_name_max_length...OK
Applyingauth.0003_alter_user_email_max_length...OK
Applyingauth.0004_alter_user_username_opts...OK
Applyingauth.0005_alter_user_last_login_null...OK
Applyingauth.0006_require_contenttypes_0002...OK
Applyingauth.0007_alter_validators_add_error_messages...OK
Applyinggames.0001_initial...OK
Applyingsessions.0001_initial...OK
Afterweruntheprecedingcommand,wewillnoticethattherootfolderforourgamesapiprojectnowhasadb.sqlite3file.WecanusetheSQLitecommandlineoranyotherapplicationthatallowsustoeasilycheckthecontentsoftheSQLitedatabasetocheckthetablesthatDjangogenerated.
InmacOSandmostmodernLinuxdistributions,SQLiteisalreadyinstalled,andtherefore,youcanrunthesqlite3command-lineutility.However,inWindows,ifyouwanttoworkwiththesqlite3.execommand-lineutility,youwillhavetodownloadandinstallSQLitefromitsWebpage-http://www.sqlite.org.
Runthefollowingcommandtolistthegeneratedtables:
sqlite3db.sqlite3'.tables'
RunthefollowingcommandtoretrievetheSQLusedtocreatethegames_gametable:
sqlite3db.sqlite3'.schemagames_game'
Thefollowingcommandwillallowyoutocheckthecontentsofthegames_gametableafterwecomposeandsendHTTPrequeststotheRESTfulAPIandmakeCRUDoperationstothegames_gametable:
sqlite3db.sqlite3'SELECT*FROMgames_gameORDERBYname;'
InsteadofworkingwiththeSQLitecommand-lineutility,youcanuseaGUItooltocheckthecontentsoftheSQLitedatabase.DBBrowserforSQLiteisausefulmultiplatformandfreeGUItoolthatallowsustoeasilycheckthedatabasecontentsofanSQLitedatabaseinmacOS,LinuxandWindows.Youcanreadmoreinformationaboutthistoolanddownloaditsdifferentversionsfromhttp://sqlitebrowser.org.Onceyouinstalledthetool,youjustneedtoopenthedb.sqlite3fileandyoucancheckthedatabasestructureandbrowsethedataforthedifferenttables.YoucanusealsothedatabasetoolsincludedinyourfavoriteIDEtocheckthecontentsfortheSQLitedatabase.
TheSQLitedatabaseengineandthedatabasefilenamearespecifiedinthegamesapi/settings.pyPythonfile.ThefollowinglinesshowthedeclarationoftheDATABASESdictionarythatcontainsthesettingsforallthedatabasethatDjangouses.Thenesteddictionarymapsthedatabasenameddefaultwiththedjango.db.backends.sqlite3databaseengineandthedb.sqlite3databasefilelocatedintheBASE_DIRfolder(gamesapi):
DATABASES={
'default':{
'ENGINE':'django.db.backends.sqlite3',
'NAME':os.path.join(BASE_DIR,'db.sqlite3'),
}
}
Afterweexecutedthemigrations,theSQLitedatabasewillhavethefollowingtables:
auth_group
auth_group_permissions
auth_permission
auth_user
auth_user_groups
auth_user_groups_permissions
django_admin_log
django_content_type
django_migrations
django_session
games_game
sqlite_sequence
Thegames_gametablepersistsinthedatabasetheGameclasswerecentlycreated,specifically,theGamemodel.Django'sintegratedORMgeneratedthegames_gametablebasedonourGamemodel.Thegames_gametablehasthefollowingrows(alsoknownasfields)withtheirSQLitetypesandallofthemarenotnullable:
id:Theintegerprimarykey,anautoincrementrowcreated:datetimename:varchar(200)release_date:datetimegame_category:varchar(200)played:bool
ThefollowinglinesshowtheSQLcreationscriptthatDjangogeneratedwhenweexecutedthemigrations:
CREATETABLE"games_game"(
"id"integerNOTNULLPRIMARYKEYAUTOINCREMENT,
"created"datetimeNOTNULL,
"name"varchar(200)NOTNULL,
"release_date"datetimeNOTNULL,
"game_category"varchar(200)NOTNULL,
"played"boolNOTNULL
)
DjangogeneratedadditionaltablesthatitrequirestosupporttheWebframeworkandtheauthenticationfeaturesthatwewilluselater.
ManagingserializationanddeserializationOurRESTfulWebAPIhastobeabletoserializeanddeserializethegameinstancesintoJSONrepresentations.WithDjangoRESTFramework,wejustneedtocreateaserializerclassforthegameinstancestomanageserializationtoJSONanddeserializationfromJSON.
DjangoRESTFrameworkusesatwo-phaseprocessforserialization.TheserializersaremediatorsbetweenthemodelinstancesandPythonprimitives.ParserandrenderershandleasmediatorsbetweenPythonprimitivesandHTTPrequestsandresponses.WewillconfigureourmediatorbetweentheGamemodelinstancesandPythonprimitivesbycreatingasubclassoftherest_framework.serializers.Serializerclasstodeclarethefieldsandthenecessarymethodstomanageserializationanddeserialization.WewillrepeatsomeoftheinformationaboutthefieldsthatwehaveincludedintheGamemodelsothatweunderstandallthethingsthatwecanconfigureinasubclassoftheSerializerclass.However,wewillworkwithshortcutsthatwillreduceboilerplatecodelaterinthenextexamples.WewillwritelesscodeinthenextexamplesbyusingtheModelSerializerclass.
Now,gotothegamesapi/gamesfolderfolderandcreateanewPythoncodefilenamedserializers.py.ThefollowinglinesshowthecodethatdeclaresthenewGameSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder.
fromrest_frameworkimportserializers
fromgames.modelsimportGame
classGameSerializer(serializers.Serializer):
pk=serializers.IntegerField(read_only=True)
name=serializers.CharField(max_length=200)
release_date=serializers.DateTimeField()
game_category=serializers.CharField(max_length=200)
played=serializers.BooleanField(required=False)
defcreate(self,validated_data):
returnGame.objects.create(**validated_data)
defupdate(self,instance,validated_data):
instance.name=validated_data.get('name',instance.name)
instance.release_date=validated_data.get('release_date',
instance.release_date)
instance.game_category=validated_data.get('game_category',
instance.game_category)
instance.played=validated_data.get('played',instance.played)
instance.save()
returninstance
TheGameSerializerclassdeclarestheattributesthatrepresentthefieldsthatwewanttobeserialized.NoticethattheyhaveomittedthecreatedattributethatwaspresentintheGamemodel.Whenthereisacalltotheinheritedsavemethodforthisclass,theoverriddencreateandupdatemethodsdefinehowtocreateormodifyaninstance.Infact,thesemethodsmust
beimplementedinourclassbecausetheyjustraiseaNotImplementedErrorexceptionintheirbasedeclaration.
Thecreatemethodreceivesthevalidateddatainthevalidated_dataargument.ThecodecreatesandreturnsanewGameinstancebasedonthereceivedvalidateddata.
TheupdatemethodreceivesanexistingGameinstancethatisbeingupdatedandthenewvalidateddataintheinstanceandvalidated_dataarguments.Thecodeupdatesthevaluesfortheattributesoftheinstancewiththeupdatedattributevaluesretrievedfromthevalidateddata,callsthesavemethodfortheupdatedGameinstanceandreturnstheupdatedandsavedinstance.
WecanlaunchourdefaultPythoninteractiveshellandmakealltheDjangoprojectmodulesavailablebeforeitstarts.Thisway,wecancheckthattheserializerworksasexpected.Inaddition,itwillhelpusunderstandinghowserializationworksinDjango.Runthefollowingcommandtolaunchtheinteractiveshell.MakesureyouarewithinthegamesapifolderintheTerminalorcommandprompt:
pythonmanage.pyshell
Youwillnoticethatalinethatsays(InteractiveConsole)isdisplayedaftertheusuallinesthatintroduceyourdefaultPythoninteractiveshell.EnterthefollowingcodeinthePythoninteractiveshelltoimportallthethingswewillneedtotesttheGamemodelanditsserializer.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
fromdatetimeimportdatetime
fromdjango.utilsimporttimezone
fromdjango.utils.siximportBytesIO
fromrest_framework.renderersimportJSONRenderer
fromrest_framework.parsersimportJSONParser
fromgames.modelsimportGame
fromgames.serializersimportGameSerializer
EnterthefollowingcodetocreatetwoinstancesoftheGamemodelandsavethem.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
gamedatetime=timezone.make_aware(datetime.now(),
timezone.get_current_timezone())
game1=Game(name='SmurfsJungle',release_date=gamedatetime,game_category='2D
mobilearcade',played=False)
game1.save()
game2=Game(name='AngryBirdsRPG',release_date=gamedatetime,game_category='3D
RPG',played=False)
game2.save()
Afterweexecutetheprecedingcode,wecanchecktheSQLitedatabasewiththepreviouslyintroducecommand-lineorGUItooltocheckthecontentsofthegames_gametable.Wewill
noticethetablehastworowsandthecolumnshavethevalueswehaveprovidedtothedifferentattributesoftheGameinstances.
EnterthefollowingcommandsintheinteractiveshelltocheckthevaluesfortheprimarykeysoridentifiersforthesavedGameinstancesandthevalueofthecreatedattributeincludesthedateandtimeinwhichwesavedtheinstancetothedatabase.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
print(game1.pk)
print(game1.name)
print(game1.created)
print(game2.pk)
print(game2.name)
print(game2.created)
Now,let'swritethefollowingcodetoserializethefirstgameinstance(game1).Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
game_serializer1=GameSerializer(game1)
print(game_serializer1.data)
Thefollowinglineshowsthegenerateddictionary,specifically,arest_framework.utils.serializer_helpers.ReturnDictinstance:
{'release_date':'2016-05-18T03:02:00.776594Z','game_category':'2Dmobile
arcade','played':False,'pk':2,'name':'SmurfsJungle'}
Now,let'sserializethesecondgameinstance(game2).Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
game_serializer2=GameSerializer(game2)
print(game_serializer2.data)
Thefollowinglineshowsthegenerateddictionary:
{'release_date':'2016-05-18T03:02:00.776594Z','game_category':'3DRPG',
'played':False,'pk':3,'name':'AngryBirdsRPG'}
WecaneasilyrenderthedictionariesholdinthedataattributeintoJSONwiththehelpoftherest_framework.renderers.JSONRendererclass.ThefollowinglinescreateaninstanceofthisclassandthencallstherendermethodtorenderthedictionariesholdinthedataattributeintoJSON.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
renderer=JSONRenderer()
rendered_game1=renderer.render(game_serializer1.data)
rendered_game2=renderer.render(game_serializer2.data)
print(rendered_game1)
print(rendered_game2)
Thefollowinglinesshowtheoutputgeneratedfromthetwocallstotherendermethod:
b'{"pk":2,"name":"SmurfsJungle","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"2Dmobilearcade","played":false}'
b'{"pk":3,"name":"AngryBirdsRPG","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"3DRPG","played":false}'
Now,wewillworkintheoppositedirection:fromserializeddatatothepopulationofaGameinstance.ThefollowinglinesgenerateanewGameinstancefromaJSONstring(serializeddata),thatis,theywilldeserialize.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile:
json_string_for_new_game='{"name":"TombRaiderExtreme
Edition","release_date":"2016-05-18T03:02:00.776594Z","game_category":"3D
RPG","played":false}'
json_bytes_for_new_game=bytes(json_string_for_new_game,encoding="UTF-8")
stream_for_new_game=BytesIO(json_bytes_for_new_game)
parser=JSONParser()
parsed_new_game=parser.parse(stream_for_new_game)
print(parsed_new_game)
ThefirstlinecreatesanewstringwiththeJSONthatdefinesanewgame(json_string_for_new_game).Then,thecodeconvertsthestringtobytesandsavestheresultsoftheconversioninthejson_bytes_for_new_gamevariable.Thedjango.utils.six.BytesIOclassprovidesabufferedI/Oimplementationusinganin-memorybytesbuffer.ThecodeusesthisclasstocreateastreamfromthepreviouslygeneratedJSONbyteswiththeserializeddata,json_bytes_for_new_game,andsavesthegeneratedinstanceinthestream_for_new_gamevariable.
WecaneasilydeserializeandparseastreamintothePythonmodelswiththehelpoftherest_framework.parsers.JSONParserclass.Thenextlinecreatesaninstanceofthisclassandthencallstheparsemethodwithstream_for_new_gameasanargument,parsesthestreamintoPythonnativedatatypesandsavestheresultsintheparsed_new_gamevariable.
Afterexecutingtheprecedinglines,parsed_new_gameholdsaPythondictionary,parsedfromthestream.Thefollowinglinesshowtheoutputgeneratedafterexecutingtheprecedingcodesnippet:
{'release_date':'2016-05-18T03:02:00.776594Z','played':False,
'game_category':'3DRPG','name':'TombRaiderExtremeEdition'}
ThefollowinglinesusetheGameSerializerclasstogenerateafullypopulatedGameinstancenamednew_gamefromthePythondictionary,parsedfromthestream.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder,intheserializers_test_01.pyfile.
new_game_serializer=GameSerializer(data=parsed_new_game)
ifnew_game_serializer.is_valid():
new_game=new_game_serializer.save()
print(new_game.name)
First,thecodecreatesaninstanceoftheGameSerializerclasswiththePythondictionarythatwepreviouslyparsedfromthestream(parsed_new_game)passedasthedatakeywordargument.Then,thecodecallstheis_validmethodtodeterminewhetherthedataisvalid.Noticethatwemustalwayscallis_validbeforeweattempttoaccesstheserializeddatarepresentationwhenwepassadatakeywordargumentinthecreationofaserializer.
Ifthemethodreturnstrue,wecanaccesstheserializedrepresentationinthedataattribute,andtherefore,thecodecallsthesavemethodthatinsertsthecorrespondingrowinthedatabaseandreturnsafullypopulatedGameinstance,savedinthenew_gamelocalvariable.Then,thecodeprintsoneoftheattributesfromthefullypopulatedGameinstance.Afterexecutingtheprecedingcode,wefullypopulatedtwoGameinstances:new_game1_instanceandnew_game2_instance.
Tip
Aswecanlearnfromtheprecedingcode,DjangoRESTFrameworkmakesiteasytoserializefromobjectstoJSONanddeserializefromJSONtoobjects,whicharecorerequirementsforourRESTfulWebAPIthathastoperformCRUDoperations.
EnterthefollowingcommandtoleavetheshellwiththeDjangoprojectmodulesthatwestartedtotestserializationanddeserialization:
quit()
WritingAPIviewsNow,wewillcreateDjangoviewsthatwillusethepreviouslycreatedGameSerializerclasstoreturnJSONrepresentationsforeachHTTPrequestthatourAPIwillhandle.Openthegames/views.pyfile.Thefollowinglinesshowtheinitialcodeforthisfile,withjustoneimportstatementandacommentthatindicatesweshouldcreatetheviews.
fromdjango.shortcutsimportrender
#Createyourviewshere.
ThefollowinglinesshowthenewcodethatcreatesaJSONResponseclassanddeclarestwofunctions:game_listandgame_detail,inthegames/views.pyfile.WearecreatingourfirstversionoftheAPI,andweusefunctionstokeepthecodeassimpleaspossible.Wewillworkwithclassesandmorecomplexcodeinthenextexamples.Thehighlightedlinesshowtheexpressionsthatevaluatethevalueoftherequest.methodattributetodeterminetheactionstobeperformedbasedontheHTTPverb.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
fromdjango.httpimportHttpResponse
fromdjango.views.decorators.csrfimportcsrf_exempt
fromrest_framework.renderersimportJSONRenderer
fromrest_framework.parsersimportJSONParser
fromrest_frameworkimportstatus
fromgames.modelsimportGame
fromgames.serializersimportGameSerializer
classJSONResponse(HttpResponse):
def__init__(self,data,**kwargs):
content=JSONRenderer().render(data)
kwargs['content_type']='application/json'
super(JSONResponse,self).__init__(content,**kwargs)
@csrf_exempt
defgame_list(request):
ifrequest.method=='GET':
games=Game.objects.all()
games_serializer=GameSerializer(games,many=True)
returnJSONResponse(games_serializer.data)
elifrequest.method=='POST':
game_data=JSONParser().parse(request)
game_serializer=GameSerializer(data=game_data)
ifgame_serializer.is_valid():
game_serializer.save()
returnJSONResponse(game_serializer.data,
status=status.HTTP_201_CREATED)
returnJSONResponse(game_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
@csrf_exempt
defgame_detail(request,pk):
try:
game=Game.objects.get(pk=pk)
exceptGame.DoesNotExist:
returnHttpResponse(status=status.HTTP_404_NOT_FOUND)
ifrequest.method=='GET':
game_serializer=GameSerializer(game)
returnJSONResponse(game_serializer.data)
elifrequest.method=='PUT':
game_data=JSONParser().parse(request)
game_serializer=GameSerializer(game,data=game_data)
ifgame_serializer.is_valid():
game_serializer.save()
returnJSONResponse(game_serializer.data)
returnJSONResponse(game_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
elifrequest.method=='DELETE':
game.delete()
returnHttpResponse(status=status.HTTP_204_NO_CONTENT)
TheJSONResponseclassisasubclassofthedjango.http.HttpResponseclass.ThesuperclassrepresentsanHTTPresponsewithastringascontent.TheJSONResponseclassrendersitscontentintoJSON.Theclassdefinesjustdeclarethe__init__methodthatcreatedarest_framework.renderers.JSONRendererinstanceandcallsitsrendermethodtorenderthereceiveddataintoJSONsavethereturnedbytestringinthecontentlocalvariable.Then,thecodeaddsthe'content_type'keytotheresponseheaderwith'application/json'asitsvalue.Finally,thecodecallstheinitializerforthebaseclasswiththeJSONbytestringandthekey-valuepairaddedtotheheader.Thisway,theclassrepresentsaJSONresponsethatweuseinthetwofunctionstoeasilyreturnaJSONresponse.
Thecodeusesthe@csrf_exemptdecoratorinthetwofunctionstoensurethattheviewsetsaCross-SiteRequestForgery(CSRF)cookie.Wedothistomakeitsimpletotestthisexamplethatdoesn'trepresentaproduction-readyWebService.WewilladdsecurityfeaturestoourRESTfulAPIlater.
WhentheDjangoserverreceivesanHTTPrequest,DjangocreatesanHttpRequestinstance,specificallyadjango.http.HttpRequestobject.Thisinstancecontainsmetadataabouttherequest,includingtheHTTPverb.ThemethodattributeprovidesastringrepresentingtheHTTPverbormethodusedintherequest.
WhenDjangoloadstheappropriateviewthatwillprocesstherequests,itpassestheHttpRequestinstanceasthefirstargumenttotheviewfunction.TheviewfunctionhastoreturnanHttpResponseinstance,specificallyadjango.http.HttpResponseinstance.
Thegame_listfunctionlistsallthegamesorcreatesanewgame.Thefunctionreceivesan
HttpRequestinstanceintherequestargument.ThefunctioniscapableofprocessingtwoHTTPverbs:GETandPOST.Thecodechecksthevalueoftherequest.methodattributetodeterminethecodetobeexecutedbasedontheHTTPverb.IftheHTTPverbisGET,theexpressionrequest.method=='GET'willevaluatetoTrueandthecodehastolistallthegames.ThecodewillretrievealltheGameobjectsfromthedatabase,usetheGameSerializertoserializeallofthem,andreturnaJSONResponseinstancebuiltwiththedatageneratedbytheGameSerializer.ThecodecreatestheGameSerializerinstancewiththemany=Trueargumenttospecifythatmultipleinstanceshavetobeserializedandnotjustone.Underthehoods,DjangousesaListSerializerwhenthemanyargumentvalueissettoTrue.
IftheHTTPverbisPOST,thecodehastocreateanewgamebasedontheJSONdatathatisincludedintheHTTPrequest.First,thecodeusesaJSONParserinstanceandcallsitsparsemethodwithrequestasanargumenttoparsethegamedataprovidedasJSONdataintherequestandsavestheresultsinthegame_datalocalvariable.Then,thecodecreatesaGameSerializerinstancewiththepreviouslyretrieveddataandcallstheis_validmethodtodeterminewhethertheGameinstanceisvalidornot.Iftheinstanceisvalid,thecodecallsthesavemethodtopersisttheinstanceinthedatabaseandreturnsaJSONResponsewiththesaveddatainitsbodyandastatusequaltostatus.HTTP_201_CREATED,thatis,201Created.
Tip
Wheneverwehavetoreturnaspecificstatusdifferentfromthedefault200OKstatus,itisagoodpracticetousethemodulevariablesdefinedintherest_framework.statusmoduleandtoavoidusinghardcodednumericvalues.
Thegame_detailfunctionretrieves,updatesordeletesanexistinggame.ThefunctionreceivesanHttpRequestinstanceintherequestargumentandtheprimarykeyoridentifierforthegametoberetrieved,updatedordeletedinthepkargument.ThefunctioniscapableofprocessingthreeHTTPverbs:GET,PUTandDELETE.Thecodechecksthevalueoftherequest.methodattributetodeterminethecodetobeexecutedbasedontheHTTPverb.NomatterwhichistheHTTPverb,thefunctioncallstheGame.objects.getmethodwiththereceivedpkasthepkargumenttoretrieveaGameinstancefromthedatabasebasedonthespecifiedprimarykeyoridentifier,andsavesitinthegamelocalvariable.Incaseagamewiththespecifiedprimarykeyoridentifierdoesn'texistinthedatabase,thecodereturnsanHttpResponsewithitsstatusequaltostatus.HTTP_404_NOT_FOUND,thatis,404NotFound.
IftheHTTPverbisGET,thecodecreatesaGameSerializerinstancewithgameasanargumentandreturnsthedatafortheserializedgameinaJSONResponsethatwillincludethedefault200OKstatus.ThecodereturnstheretrievedgameserializedasJSON.
IftheHTTPverbisPUT,thecodehastocreateanewgamebasedontheJSONdatathatisincludedintheHTTPrequestanduseittoreplaceanexistinggame.First,thecodeusesaJSONParserinstanceandcallsitsparsemethodwithrequestasanargumenttoparsethegamedataprovidedasJSONdataintherequestandsavestheresultsinthegame_datalocalvariable.Then,thecodecreatesaGameSerializerinstancewiththeGameinstancepreviouslyretrieved
fromthedatabase(game)andtheretrieveddatathatwillreplacetheexistingdata(game_data).Then,thecodecallstheis_validmethodtodeterminewhethertheGameinstanceisvalidornot.Iftheinstanceisvalid,thecodecallsthesavemethodtopersisttheinstancewiththereplacedvaluesinthedatabaseandreturnsaJSONResponsewiththesaveddatainitsbodyandthedefault200OKstatus.Iftheparseddatadoesn'tgenerateavalidGameinstance,thecodereturnsaJSONResponsewithastatusequaltostatus.HTTP_400_BAD_REQUEST,thatis,400BadRequest.
IftheHTTPverbisDELETE,thecodecallsthedeletemethodfortheGameinstancepreviouslyretrievedfromthedatabase(game).Thecalltothedeletemethoderasestheunderlyingrowinthegames_gametable,andtherefore,thegamewon'tbeavailableanymore.Then,thecodereturnsaJSONResponsewithastatusequaltostatus.HTTP_204_NO_CONTENTthatis,204NoContent.
Now,wehavetocreateanewPythonfilenamedurls.pyinthegamesfolder,specifically,thegames/urls.pyfile.ThefollowinglinesshowthecodeforthisfilethatdefinestheURLpatternsthatspecifiestheregularexpressionsthathavetobematchedintherequesttorunaspecificfunctiondefinesintheviews.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
fromdjango.conf.urlsimporturl
fromgamesimportviews
urlpatterns=[
url(r'^games/$',views.game_list),
url(r'^games/(?P<pk>[0-9]+)/$',views.game_detail),
]
TheurlpatternslistmakesitpossibletorouteURLstoviews.Thecodecallsthedjango.conf.urls.urlfunctionwiththeregularexpressionthathastobematchedandtheviewfunctiondefinedintheviewsmoduleasargumentstocreateaRegexURLPatterninstanceforeachentryintheurlpatternslist.
Wehavetoreplacethecodeintheurls.pyfileinthegamesapifolder,specifically,thegamesapi/urls.pyfile.ThefiledefinestherootURLconfigurations,andtherefore,wemustincludetheURLpatternsdeclaredinthepreviouslycodedgames/urls.pyfile.Thefollowinglinesshowthenewcodeforthegamesapi/urls.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_01_01folder:
fromdjango.conf.urlsimporturl,include
urlpatterns=[
url(r'^',include('games.urls')),
]
Now,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequeststoourunsecureWebAPI(wewilldefinitelyaddsecuritylater).Executethefollowingcommand:
pythonmanage.pyrunserver
Thefollowinglinesshowtheoutputafterweexecutetheprecedingcommand.Thedevelopmentserverislisteningatport8000.
Performingsystemchecks...
Systemcheckidentifiednoissues(0silenced).
May20,2016-04:22:38
Djangoversion1.9.6,usingsettings'gamesapi.settings'
Startingdevelopmentserverathttp://127.0.0.1:8000/
QuittheserverwithCONTROL-C.
Withtheprecedingcommand,wewillstartDjangodevelopmentserverandwewillonlybeabletoaccessitinourdevelopmentcomputer.TheprecedingcommandstartsthedevelopmentserverinthedefaultIPaddress,thatis,127.0.0.1(localhost).ItisnotpossibletoaccessthisIPaddressfromothercomputersordevicesconnectedonourLAN.Thus,ifwewanttomakeHTTPrequeststoourAPIfromothercomputersordevicesconnectedtoourLAN,weshouldusethedevelopmentcomputerIPaddress,0.0.0.0(forIPv4configurations),or::(forIPv6configurations)asthedesiredIPaddressforourdevelopmentserver.
Ifwespecify0.0.0.0asthedesiredIPaddressforIPv4configurations,thedevelopmentserverwilllistenoneveryinterfaceonport8000.Whenwespecify::forIPv6configurations,itwillhavethesameeffect.Inaddition,itisnecessarytoopenthedefaultport8000inourfirewalls(softwareand/orhardware)andconfigureport-forwardingtothecomputerthatisrunningthedevelopmentserver.ThefollowingcommandlaunchesDjango'sdevelopmentserverinanIPv4configurationandallowsrequeststobemadefromothercomputersanddevicesconnectedtoourLAN:
pythonmanage.pyrunserver0.0.0.0:8000
Tip
IfyoudecidetocomposeandsendHTTPrequestsfromothercomputersordevicesconnectedtotheLAN,rememberthatyouhavetousethedevelopmentcomputer'sassignedIPaddressinsteadoflocalhost.Forexample,ifthecomputer'sassignedIPv4IPaddressis192.168.1.106,insteadoflocalhost:8000,youshoulduse192.168.1.106:8000.Ofcourse,youcanalsousethehostnameinsteadoftheIPaddress.ThepreviouslyexplainedconfigurationsareveryimportantbecausemobiledevicesmightbetheconsumersofourRESTfulAPIsandwewillalwayswanttotesttheappsthatmakeuseofourAPIsinourdevelopmentenvironments.
MakingHTTPrequeststotheAPITheDjangodevelopmentserverisrunningonlocalhost(127.0.0.1),listeningonport8000,andwaitingforourHTTPrequests.Now,wewillcomposeandsendHTTPrequestslocallyinourdevelopmentcomputerorfromothercomputerordevicesconnectedtoourLAN.WewillusethefollowingdifferentkindoftoolstocomposeandsendHTTPrequeststhroughoutourbook.
Command-linetoolsGUItoolsPythoncodeJavaScriptcode
Tip
NoticethatyoucanuseanyotherapplicationthatallowsyoutocomposeandsendHTTPrequests.Therearemanyappsthatrunontabletsandsmartphonesthatallowyoutoaccomplishthistask.However,wewillfocusourattentiononthemostusefultoolswhenbuildingRESTfulWebAPIs.
Workingwithcommand-linetools-curlandhttpieWewillstartwithcommand-linetools.Oneofthekeyadvantagesofcommand-linetoolsisthatwecaneasilyrunagaintheHTTPrequestsafterwebuiltthemforthefirsttime,andwedon'tneedtousethemouseortapthescreentorunrequests.Wecanalsoeasilybuildascriptwithbatchrequestsandrunthem.Ashappenswithanycommand-linetool,itcantakemoretimetoperformthefirstrequestscomparedwithGUItools,butitbecomeseasieronceweperformedmanyrequestsandwecaneasilyreusethecommandswehavewritteninthepasttocomposenewrequests.
Curl,alsoknownascURL,isaverypopularopensourcecommand-linetoolandlibrarythatallowustoeasilytransferdata.Wecanusethecurlcommand-linetooltoeasilycomposeandsendHTTPrequestsandchecktheirresponses.
Tip
IfyouareworkingoneithermacOSorLinux,youcanopenaTerminalandstartusingcurlfromthecommandline.IfyouareworkingonanyWindowsversion,youcaneasilyinstallcurlfromtheCygwinpackageinstallationoption,andexecuteitfromtheCygwinterminal.Youcanreadmoreaboutthecurlutilityathttp://curl.haxx.se.YoucanreadmoreabouttheCygwinterminalanditsinstallationprocedureathttp://cygwin.com/install.html.
OpenaCygwinterminalinWindowsoraterminalinmacOSorLinux,andrunthefollowingcommand.Itisveryimportantthatyouentertheendingslash(/)because/gameswon'tmatchanyofthepatternsspecifiedinurlpatternsinthegames/urls.pyfile.WeareusingthedefaultconfigurationforDjangothatdoesn'tredirectURLsthatdon'tmatchanyofthepatternstothesameURLswithaslashappended.Thus,wemustenter/games/,includingtheendingslash(/):
curl-XGET:8000/games/
TheprecedingcommandwillcomposeandsendthefollowingHTTPrequest-GEThttp://localhost:8000/games/.TherequestisthesimplestcaseinourRESTfulAPIbecauseitwillmatchandruntheviews.game_listfunction,thatis,thegame_listfunctiondeclaredwithinthegames/views.pyfile.ThefunctionjustreceivesrequestasaparameterbecausetheURLpatterndoesn'tincludeanyparameters.AstheHTTPverbfortherequestisGET,therequest.methodpropertyisequalto'GET',andtherefore,thefunctionwillexecutethecodethatretrievesalltheGameobjectsandgeneratesaJSONresponsewithalloftheseGameobjectsserialized.
ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthreeGameobjectsintheJSONresponse:
[{"pk":3,"name":"AngryBirdsRPG","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"3DRPG","played":false},
{"pk":2,"name":"SmurfsJungle","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"2Dmobilearcade","played":false},
{"pk":11,"name":"TombRaiderExtremeEdition","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"3DRPG","played":false}]
Aswemightnoticefromthepreviousresponse,thecurlutilitydisplaystheJSONresponseinasingleline,andtherefore,itisabitdifficulttoreadit.Inthiscase,weknowthattheContent-Typeoftheresponseisapplication/json.However,incasewewanttohavemoredetailsabouttheresponse,wecanusethe-ioptiontorequestcurltoprinttheHTTPresponseheaders.Wecancombinethe-iand-Xoptionsbyusing-iX.
GobacktotheCygwinterminalinWindowsortheTerminalinmacOSorLinux,andrunthefollowingcommand:
curl-iXGET:8000/games/
ThefollowinglinesshowanexampleresponsefortheHTTPrequest.ThefirstlinesshowtheHTTPresponseheaders,includingthestatus(200OK)andtheContent-type(application/json).AftertheHTTPresponseheaders,wecanseethedetailsforthethreeGameobjectsintheJSONresponse:
HTTP/1.0200OK
Date:Tue,24May201618:04:40GMT
Server:WSGIServer/0.2CPython/3.5.1
Content-Type:application/json
X-Frame-Options:SAMEORIGIN
[{"pk":3,"name":"AngryBirdsRPG","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"3DRPG","played":false},
{"pk":2,"name":"SmurfsJungle","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"2Dmobilearcade","played":false},
{"pk":11,"name":"TombRaiderExtremeEdition","release_date":"2016-05-
18T03:02:00.776594Z","game_category":"3DRPG","played":false}]
Afterwerunthetworequests,wewillseethefollowinglinesinthewindowthatisrunningtheDjangodevelopmentserver.TheoutputindicatesthattheserverreceivedtwoHTTPrequestswiththeGETverband/games/astheURI.TheserverprocessedbothHTTPrequests,returnedstatuscode200andtheresponselengthwasequalto379characters.Theresponselengthcanbedifferentbecausethevaluefortheprimarykeyassignedtoeachgamewillhaveanincidenceintheresponselength.ThefirstnumberafterHTTP/1.1."indicatesthereturnedstatuscode(200)andthesecondnumbertheresponselength(379).
[25/May/201604:35:09]"GET/games/HTTP/1.1"200379
[25/May/201604:35:10]"GET/games/HTTP/1.1"200379
Thefollowingimageshowstwoterminalwindowsside-by-sideonmacOS.TheTerminalwindowattheleft-handsideisrunningtheDjangodevelopmentserveranddisplaysthereceivedandprocessedHTTPrequests.TheTerminalwindowattheright-handsideisrunningcurlcommandstogeneratetheHTTPrequests.
Itisagoodideatouseasimilarconfigurationtochecktheoutputwhilewecomposeandsend
theHTTPrequests.NoticethattheJSONoutputsareabitdifficulttoreadbecausetheydon'tusesyntaxhighlighting:
Now,wewillinstallHTTPie,acommand-lineHTTPclientwritteninPythonthatmakesiteasytosendHTTPrequestsandusesasyntaxthatiseasierthancurl(alsoknownascURL).OneofthegreatadvantagesofHTTPieisthatitdisplayscolorizedoutputandusesmultiplelinestodisplaytheresponsedetails.Thus,HTTPiemakesiteasiertounderstandtheresponsesthanthecurlutility.WejustneedtoactivatethevirtualenvironmentandthenrunthefollowingcommandintheterminalorcommandprompttoinstalltheHTTPiepackage:
pipinstall--upgradehttpie
Thelastlinesfortheoutputwillindicatethatthedjangopackagehasbeensuccessfullyinstalled.
Collectinghttpie
Downloadinghttpie-0.9.3-py2.py3-none-any.whl(66kB)
Collectingrequests>=2.3.0(fromhttpie)
Usingcachedrequests-2.10.0-py2.py3-none-any.whl
CollectingPygments>=1.5(fromhttpie)
UsingcachedPygments-2.1.3-py2.py3-none-any.whl
Installingcollectedpackages:requests,Pygments,httpie
SuccessfullyinstalledPygments-2.1.3httpie-0.9.3requests-2.10.0
Tip
Incaseyoudon'trememberhowtoactivatethevirtualenvironmentthatwecreatedforthisexample,readthefollowingsectioninthischapter-SettingupthevirtualenvironmentwithDjangoRESTframework.
Now,wecanuseanhttpcommandtoeasilycomposeandsendHTTPrequeststo
localhost:8000andtesttheRESTfulAPIbuiltwithDjangoRESTframework.HTTPiesupportscurl-likeshorthandsforlocalhost,andtherefore,wecanuse:8000asashorthandthatexpandstohttp://localhost:8000.Runthefollowingcommandandremembertoentertheendingslash(/):
http:8000/games/
TheprecedingcommandwillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/games/.Therequestisthesameonewehavepreviouslycomposedwiththecurlcommand.However,inthiscase,theHTTPieutilitywilldisplayacolorizedoutputanditwillusemultiplelinestodisplaytheJSONresponse.TheprecedingcommandisequivalenttothefollowingcommandthatspecifiestheGETmethodafterhttp:
httpGET:8000/games/
ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withtheheadersandthethreeGameobjectsintheJSONresponse.ItisindeedeasiertounderstandtheresponsecomparedwiththeresultsgeneratedwhenwecomposedtheHTTPrequestwithcurl.HTTPieautomaticallyformatstheJSONdatareceivedasaresponseandappliessyntaxhighlighting,specifically,bothcolorsandformatting:
HTTP/1.0200OK
Content-Type:application/json
Date:Thu,26May201621:33:17GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
[
{
"game_category":"3DRPG",
"name":"AngryBirdsRPG",
"pk":3,
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
},
{
"game_category":"2Dmobilearcade",
"name":"SmurfsJungle",
"pk":2,
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
},
{
"game_category":"3DRPG",
"name":"TombRaiderExtremeEdition",
"pk":11,
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
]
Tip
Wecanachievethesameresultsbycombiningtheoutputgeneratedwiththecurlcommandwithotherutilities.However,HTTPieprovidesusexactlywhatweneedtoworkwithRESTfulAPIs.WewilluseHTTPietocomposeandsendHTTPrequest,butwewillalwaysprovidetheequivalentcurlcommand.
ThefollowingimageshowstwoTerminalwindowsside-by-sideonmacOS.Theterminalwindowattheleft-handsideisrunningtheDjangodevelopmentserveranddisplaysthereceivedandprocessedHTTPrequests.TheTerminalwindowattheright-handsideisrunningHTTPiecommandstogeneratetheHTTPrequests.NoticethattheJSONoutputiseasiertoreadcomparedtotheoutputgeneratedbythecurlcommand:
WecanexecuteHTTPiewiththe-boptionincasewedon'twanttoincludetheheaderintheresponse.Forexample,thefollowinglineperformsthesameHTTPrequestbutdoesn'tdisplaytheheaderintheresponseoutput,andtherefore,theoutputwilljustdisplaytheJSONresponse:
http-b:8000/games/
Now,wewillselectoneofthegamesfromtheprecedinglistandwewillcomposeanHTTPrequesttoretrievejustthechosengame.Forexample,inthepreviouslist,thefirstgamehasapkvalueequalto3.Runthefollowingcommandtoretrievethisgame.Usethepkvalueyouhaveretrievedinthepreviouscommandforthefirstgame,asthepknumbermightbedifferent:
http:8000/games/3/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/games/3/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/games/3/.Therequesthasanumberafter/games/,andtherefore,itwillmatch'^games/(?P<pk>[0-9]+)/$'andruntheviews.game_detailfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.ThefunctionreceivesrequestandpkasparametersbecausetheURLpatternpassesthenumberspecifiedafter/games/inthepkparameter.AstheHTTPverbfortherequestisGET,therequest.methodpropertyisequalto'GET',andtherefore,thefunctionwillexecutethecodethatretrievestheGameobjectwhoseprimarykeymatchesthepkvaluereceivedasanargumentand,iffound,generatesaJSONresponsewiththisGameobjectserialized.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withtheGameobjectthatmatchesthepkvalueintheJSONresponse:
HTTP/1.0200OK
Content-Type:application/json
Date:Fri,27May201602:28:30GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
{
"game_category":"3DRPG",
"name":"AngryBirdsRPG",
"pk":3,
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
Now,wewillcomposeandsendanHTTPrequesttoretrieveagamethatdoesn'texist.Forexample,intheprecedinglist,thereisnogamewithapkvalueequalto99999.Runthefollowingcommandtotrytoretrievethisgame.Makesureyouuseapkvaluethatdoesn'texist.Wemustmakesurethattheutilitiesdisplaytheheadersaspartoftheresponsebecausetheresponsewon'thaveabody:
http:8000/games/99999/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/games/99999/
TheprecedingcommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/games/99999/.Therequestisthesamethanthepreviousonewehaveanalyzed,withadifferentnumberforthepkparameter.Theserverwillruntheviews.game_detailfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.ThefunctionwillexecutethecodethatretrievestheGameobjectwhoseprimarykeymatchesthepkvaluereceivedasanargumentandaGame.DoesNotExistexceptionwillbethrownandcapturedbecausethereisnogamewiththespecifiedpkvalue.Thus,thecodewillreturnanHTTP404NotFoundstatuscode.ThefollowinglinesshowanexampleheaderresponsefortheHTTPrequest:
HTTP/1.0404NotFound
Content-Type:text/html;charset=utf-8
Date:Fri,27May201602:20:41GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
WewillcomposeandsendanHTTPrequesttocreateanewgame.
httpPOST:8000/games/name='PvZ3'game_category='2Dmobilearcade'
played=falserelease_date='2016-05-18T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand.Itisveryimportanttousethe-H"Content-Type:application/json"optiontoindicatecurltosendthedataspecifiedafterthe-doptionasapplication/jsoninsteadofthedefaultapplication/x-www-form-urlencoded:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"PvZ3",
"game_category":"2Dmobilearcade","played":"false","release_date":"2016-
05-18T03:02:00.776594Z"}':8000/games/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:POSThttp://localhost:8000/games/withthefollowingJSONkey-valuepairs:
{
"name":"PvZ3",
"game_category":"2Dmobilearcade",
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
Therequestspecifies/games/,andtherefore,itwillmatch'^games/$'andruntheviews.game_listfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.ThefunctionjustreceivesrequestasaparameterbecausetheURLpatterndoesn'tincludeanyparameters.AstheHTTPverbfortherequestisPOST,therequest.methodpropertyisequalto'POST',andtherefore,thefunctionwillexecutethecodethatparsestheJSONdatareceivedintherequest,createsanewGameand,ifthedataisvalid,itsavesthenewGame.IfthenewGamewassuccessfullypersistedinthedatabase,thefunctionreturnsanHTTP201CreatedstatuscodeandtherecentlypersistedGameserializedserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewGameobjectintheJSONresponse:
HTTP/1.0201Created
Content-Type:application/json
Date:Fri,27May201605:12:39GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
{
"game_category":"2Dmobilearcade",
"name":"PvZ3",
"pk":15,
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
Now,wewillcomposeandsendanHTTPrequesttoupdateanexistinggame,specifically,thepreviouslyaddedgame.Wehavetocheckthevalueassignedtopkinthepreviousresponseandreplace15inthecommandwiththereturnedvalue.Forexample,incasethevalueforpkwas5,youshoulduse:8000/games/5/insteadof:8000/games/15/.
httpPUT:8000/games/15/name='PvZ3'game_category='2Dmobilearcade'
played=truerelease_date='2016-05-20T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand.Ashappenedwiththepreviouscurlexample,itisveryimportanttousethe-H"Content-Type:application/json"optiontoindicatecurltosendthedataspecifiedafterthe-doptionasapplication/jsoninsteadofthedefaultapplication/x-www-form-urlencoded:
curl-iXPUT-H"Content-Type:application/json"-d'{"name":"PvZ3",
"game_category":"2Dmobilearcade","played":"true","release_date":"2016-
05-20T03:02:00.776594Z"}':8000/games/15/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:PUThttp://localhost:8000/games/15/withthefollowingJSONkey-valuepairs:
{
"name":"PvZ3",
"game_category":"2Dmobilearcade",
"played":true,
"release_date":"2016-05-20T03:02:00.776594Z"
}
Therequesthasanumberafter/games/,andtherefore,itwillmatch'^games/(?P<pk>[0-9]+)/$'andruntheviews.game_detailfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.ThefunctionreceivesrequestandpkasparametersbecausetheURLpatternpassesthenumberspecifiedafter/games/inthepkparameter.AstheHTTPverbfortherequestisPUT,therequest.methodpropertyisequalto'PUT',andtherefore,thefunctionwillexecutethecodethatparsestheJSONdatareceivedintherequest,createsaGameinstancefromthisdataandupdatestheexistinggameinthedatabase.Ifthegamewassuccessfullyupdatedinthedatabase,thefunctionreturnsanHTTP200OKstatuscodeandtherecentlyupdatedGameserializedserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withtheupdatedGameobjectintheJSONresponse:
HTTP/1.0200OK
Content-Type:application/json
Date:Sat,28May201600:49:05GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
{
"game_category":"2Dmobilearcade",
"name":"PvZ3",
"pk":15,
"played":true,
"release_date":"2016-05-20T03:02:00.776594Z"
}
InordertosuccessfullyprocessaPUTHTTPrequestthatupdatesanexistinggame,wemustprovidevaluesforalltherequiredfields.WewillcomposeandsendanHTTPrequesttotryupdateanexistinggame,andwewillfailtodosobecausewewilljustprovideavalueforthename.Ashappenedinthepreviousrequest,wewillusethevalueassignedtopkinthelastgameweadded:
httpPUT:8000/games/15/name='PvZ4'
Thefollowingistheequivalentcurlcommand:
curl-iXPUT-H"Content-Type:application/json"-d'{"name":"PvZ4"}'
:8000/games/15/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:PUThttp://localhost:8000/games/15/withthefollowingJSONkey-valuepair:
{
"name":"PvZ4",
}
Therequestwillexecutethesamecodeweexplainedforthepreviousrequest.Becausewedidn'tprovidealltherequiredvaluesforaGameinstance,thegame_serializer.is_valid()methodwillreturnFalseandthefunctionwillreturnanHTTP400BadRequeststatuscodeandthedetailsgeneratedinthegame_serializer.errorsattributeserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withtherequiredfieldsthatourrequestdidn'tincludevaluesintheJSONresponse:
HTTP/1.0400BadRequest
Content-Type:application/json
Date:Sat,28May201602:53:08GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
{
"game_category":[
"Thisfieldisrequired."
],
"release_date":[
"Thisfieldisrequired."
]
}
Tip
WhenwewantourAPItobeabletoupdateasinglefieldforanexistingresource,inthiscase,anexistinggame,weshouldprovideanimplementationforthePATCHmethod.ThePUTmethodismeanttoreplaceanentireresourceandthePATCHmethodismeanttoapplyadeltatoanexistingresource.WecanwritecodeinthehandlerforthePUTmethodapplyadeltatoanexistingresource,butitisabetterpracticetousethePATCHmethodforthisspecifictask.WewillworkwiththePATCHmethodlater.
Now,wewillcomposeandsendanHTTPrequesttodeleteanexistinggame,specifically,thelastgameweadded.AshappenedinourlastHTTPrequests,wehavetocheckthevalueassignedtopkinthepreviousresponseandreplace12inthecommandwiththereturnedvalue:
httpDELETE:8000/games/15/
Thefollowingistheequivalentcurlcommand:
curl-iXDELETE:8000/games/15/
TheprecedingcommandswillcomposeandsendthefollowingHTTPrequest:DELETEhttp://localhost:8000/games/15/.Therequesthasanumberafter/games/,andtherefore,itwillmatch'^games/(?P<pk>[0-9]+)/$'andruntheviews.game_detailfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.ThefunctionreceivesrequestandpkasparametersbecausetheURLpatternpassesthenumberspecifiedafter/games/inthepkparameter.AstheHTTPverbfortherequestisDELETE,therequest.methodpropertyisequalto'DELETE',andtherefore,thefunctionwillexecutethecodethatparsestheJSONdatareceivedintherequest,createsaGameinstancefromthisdataanddeletestheexistinggameinthedatabase.Ifthegamewassuccessfullydeletedinthedatabase,thefunctionreturnsanHTTP204NoContentstatuscode.ThefollowinglinesshowanexampleresponsefortheHTTPrequestaftersuccessfullydeletinganexistinggame:
HTTP/1.0204NoContent
Date:Sat,28May201604:08:58GMT
Server:WSGIServer/0.2CPython/3.5.1
Content-Length:0
X-Frame-Options:SAMEORIGIN
Content-Type:text/html;charset=utf-8
WorkingwithGUItools-PostmanandothersSofar,wehavebeenworkingwithtwoterminal-basedorcommand-linetoolstocomposeandsendHTTPrequeststoourDjangodevelopmentserver-cURLandHTTPie.Now,wewillworkwithGUI(GraphicalUserInterface)tools.
PostmanisaverypopularAPItestingsuiteGUItoolthatallowsustoeasilycomposeandsendHTTPrequests,amongotherfeatures.PostmanisavailableasaChromeAppandasaMacApp.WecanexecuteitinWindows,LinuxandmacOSasaChromeApp,thatis,anapplicationrunningontopofGoogleChrome.IncaseweworkwithmacOS,wecanusetheMacAppinsteadoftheChromeApp.YoucandownloadtheversionsofthePostmanAppfromthefollowingURL-https://www.getpostman.com.
Tip
YoucandownloadandinstallPostmanforfreetocomposeandsendHTTPrequeststoourRESTfulAPIs.YoujustneedtosignuptoPostmanandwewon'tbeusinganyofthepaidfeaturesprovidedbyPostmancloudinourexamples.AlltheinstructionsworkwithPostman4.2.2orgreater.
Now,wewillusetheBuildertabinPostmantoeasilycomposeandsendHTTPrequeststolocalhost:8000andtesttheRESTfulAPIwiththisGUItool.Postmandoesn'tsupportcurl-likeshorthandsforlocalhost,andtherefore,wecannotusethesameshorthandswehavebeenusingwhencomposingrequestswithHTTPie.
SelectGET inthedropdownmenuattheleft-handsideoftheEnterrequestURLtextbox,andenterlocalhost:8000/games/inthistextboxattheright-handsideofthedropdown.Then,clickSendandPostmanwilldisplaytheStatus(200OK),thetimeittookfortherequesttobeprocessedandtheresponsebodywithallthegamesformattedasJSONwithsyntaxhighlighting(Prettyview).
ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPGETrequest:
ClickonHeadersattheright-handsideofBodyandCookiestoreadtheresponseheaders.ThefollowingscreenshotshowsthelayoutfortheresponseheadersthatPostmandisplaysfortheprecedingresponse.NoticethatPostmandisplaystheStatusattheright-handsideoftheresponseanddoesn'tincludeitasthefirstlineoftheHeaders,ashappenedwhenweworkedwithboththecURLandHTTPieutilities:
Now,wewillusetheBuildertabinPostmantocomposeandsendanHTTPrequesttocreateanewgame,specifically,aPOSTrequest.Followthenextsteps:
1. SelectPOST inthedrop-downmenuattheleft-handsideoftheEnterrequestURLtextbox,andenterlocalhost:8000/games/inthistextboxattheright-handsideofthedropdown.
2. ClickBodyattheright-handsideofAuthorizationandHeaders,withinthepanelthatcomposestherequest.
3. ActivatetherawradiobuttonandselectJSON(application/json)inthedropdownattheright-handsideofthebinaryradiobutton.PostmanwillautomaticallyaddaContent-typeasapplication/jsonheader,andtherefore,youwillnoticetheHeaderstabwillberenamedtoHeaders(1),indicatingusthatthereisonekey-valuepairspecifiedfortherequestheaders.
4. Enterthefollowinglinesinthetextboxbelowtheradiobuttons,withintheBodytab:
{
"name":"BatmanvsSuperman",
"game_category":"3DRPG",
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
ThefollowingscreenshotshowstherequestbodyinPostman:
WefollowedthenecessarystepstocreateanHTTPPOSTrequestwithaJSONbodythatspecifiesthenecessarykey-valuepairstocreateanewgame.ClickonSendandPostmanwilldisplaytheStatus(201Created),thetimeittookfortherequesttobeprocessedandtheresponsebodywiththerecentlyaddedgameformattedasJSONwithsyntaxhighlighting(Prettyview).ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPPOSTrequest.
Tip
IfwewanttocomposeandsendanHTTPPUTrequestwithPostman,itisnecessarytofollowthepreviouslyexplainedstepstoprovideJSONdatawithintherequestbody.
OneofthenicefeaturesincludedinPostmanisthatwecaneasilyreviewandagainruntheHTTPrequestswehavemadebybrowsingthesavedHistoryshownattheleft-handsideofthePostmanwindow.TheHistorypanedisplaysalistwiththeHTTPverbfollowedbytheURLforeachHTTPrequestwehavecomposedandsent.WejustneedtoclickonthedesiredHTTPrequestandclickSendtorunitagain.ThefollowingscreenshotshowsthemanyHTTPrequestsintheHistorypaneandthefirstoneselectedtosenditagain.
JetBrainsPyCharmisaverypopularmultiplatformPythonIDE(shortforIntegratedDevelopmentEnvironment)availableonmacOS,LinuxandWindows.ItspaidProfessionalversionincludesaRESTClientthatallowsustotestRESTfulWebservices.IncaseweworkwiththisversionoftheIDE,wecancomposeandsendHTTPrequestswithoutleavingtheIDE.Youdon'tneedaJetBrainsPyCharmProfessionalversionlicensetoruntheexamplesincludedinthisbook.However,astheIDEisverypopular,wewilllearnthenecessarystepstocomposeandsendanHTTPrequestforourAPIusingtheRESTClientincludedinthisIDE.
Now,wewillusetheRESTClientincludedinPyCharmprofessionaltocomposeandsendanHTTPrequesttocreateanewgame,specifically,aPOSTrequest.Followthenextsteps:
1. SelectTools|TestRESTfulWebServiceinthemainmenutodisplaytheRESTClientpanel.
2. SelectPOST intheHTTPmethoddropdownmenuintheRESTClientpane.3. Enterlocalhost:8000intheHost/porttextbox,attheright-handsideofthedropdown.4. Enter/games/inthePathtextbox,attheright-handsideoftheHost/porttextbox.5. MakesurethattheRequesttabisactivatedandclickontheadd(+)buttonatthebottom
oftheHeaderslist.TheIDEwilldisplayatextboxforthenameandadropdownforthevalue.EnterContent-TypeinName,enterapplication/jsoninValueandpressEnter.
6. ActivatetheText:radiobuttoninRequestBodyandclickthe...button,ontheright-handsideoftheTexttextbox,tospecifythetexttosend.EnterthefollowinglinesintextboxincludedintheSpecifythetexttosenddialogboxandthenclickonOK.
{
"name":"TeenageMutantNinjaTurtles",
"game_category":"3DRPG",
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
ThefollowingscreenshotshowstherequestbuiltinPyCharmProfessionalRESTClient:
WefollowedthenecessarystepstocreateanHTTPPOSTrequestwithaJSONbodythatspecifiesthenecessarykey-valuepairstocreateanewgame.Clickonthesubmitrequestbutton,thatis,thefirstbuttonwiththeplayiconattheupper-leftcorneroftheRESTClientpane.TheRESTclientwillcomposeandsendtheHTTPPOSTrequest,willactivatetheResponsetab,anddisplaytheresponsecode201(Created),thetimeittookfortherequesttobeprocessed,andthecontentlengthatthebottomofthepane.
Bydefault,theRESTclientwillautomaticallyapplyJSONsyntaxhighlightingtotheresponse.However,sometimes,theJSONcontentisdisplayedwithoutlinebreaksanditisnecessarytoclickonthereformatresponsebutton,thatis,thefirstbuttonintheResponsetab.TheRESTclientdisplaystheresponseheadersinanothertab,andtherefore,itjustdisplaystheresponsebodyintheResponsetab.ThefollowingscreenshotshowstheJSONresponsebodyintheRESTclientfortheHTTPPOSTrequest:
Tip
IfwewanttocomposeandsendanHTTPPUTrequestwiththeRESTClientincludedinPyCharmProfessional,itisnecessarytofollowthepreviouslyexplainedstepstoprovideJSONdatawithintherequestbody.
Incaseyoudon'tworkwithPyCharmProfessional,runanyofthefollowingcommandstocomposeandsendtheHTTPPOSTrequesttocreatethenewgame:
httpPOST:8000/games/name='TeenageMutantNinjaTurtles'game_category='3D
RPG'played=falserelease_date='2016-05-18T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Teenage
MutantNinjaTurtles","game_category":"3DRPG","played":"false",
"release_date":"2016-05-18T03:02:00.776594Z"}':8000/games/
TelerikFiddlerisapopulartoolforWindowsdevelopers.TelerikFiddlerisafreeWebdebuggingproxywithaGUIbutitonlyrunsonWindows.ItsmainWebpagepromotesitasamulti-platformtool,butatthetimethisbookwaspublished,themacOSandLinuxversionswerecompletelyunstableandtheirdevelopmentabandoned.WecanuseTelerikFiddlerinWindowstocomposeandsendHTTPrequests,amongotherfeatures.YoucandownloadFiddlerforWindowsfromthefollowingURL-https://www.telerik.com/download/fiddler.
StoplightisapopularpowerfulAPImodelingtoolthatallowsustoeasilytestourAPIs.ItsHTTPrequestmakerallowsustocomposeandsendrequestsandgeneratethenecessarycodetomakethemindifferentprogramminglanguages,suchasJavaScript,Swift,C#,PHP,Node,andGo,amongothers.YoucansignuptoworkwithStoplightatthefollowingURL-http://stoplight.io.
WecanalsouseappsthatcancomposeandsendHTTPrequestsfrommobiledevicestoworkwiththeRESTfulAPI.Forexample,wecanworkwiththeiCurlHTTPApponiOSdevicessuchasiPadandiPhone-https://itunes.apple.com/us/app/icurlhttp/id611943891?mt=8.InAndroiddevices,wecanworkwiththeHTTPRequestApp-https://play.google.com/store/apps/details?id=air.http.request&hl=en.
ThefollowingscreenshotshowstheresultsofcomposingandsendingthefollowingHTTPrequestwiththeiCurlHTTPApp:GEThttp://192.168.1.106:8000/games/.RememberthatyouhavetoperformthepreviouslyexplainedconfigurationsinyourLANandroutertobeabletoaccesstheDjangodevelopmentserverfromotherdevicesconnectedtoyourLAN.Inthiscase,theIPassignedtothecomputerrunningtheDjangoWebserveris192.168.1.106,andtherefore,youmustreplacethisIPwiththeIPassignedtoyourdevelopmentcomputer.
Atthetimethisbookwaspublished,themobileappsthatallowyoutocomposeandsendHTTPrequestsdonotprovideallthefeaturesyoucanfindinPostmanorcommand-lineutilities.
Testyourknowledge1. IfwewanttocreateasimplePlayermodelthatwewillusetorepresentandpersist
playersinDjangoRESTframework,wecancreate:1. APlayerclassasasubclassofthedjangorestframework.models.Modelclass.2. APlayerclassasasubclassofthedjango.db.models.Modelclass.3. APlayerfunctionintherestframeworkmodels.pyfile.
2. IntheDjangoRESTFramework,serializersare:1. MediatorsbetweenthemodelinstancesandPythonprimitives.2. MediatorsbetweentheviewfunctionsandPythonprimitives.3. MediatorsbetweentheURLsandviewfunctions.
3. IntheDjangoRESTFramework,parsersandrenderers:1. HandleasmediatorsbetweenmodelinstancesandPythonprimitives.2. Resettheboard.3. HandleasmediatorsbetweenPythonprimitivesandHTTPrequestsandresponses.
4. Theurlpatternslistdeclaredintheurls.pyfilemakesitpossibleto:1. RouteURLstoviews.2. RouteURLstomodels.3. RouteURLstoPythonprimitives.
5. HTTPieisa:1. Command-lineHTTPserverwritteninPythonthatmakesiteasytocreatea
RESTfulWebServer.2. Command-lineutilitythatallowsustorunqueriesagainstanSQLitedatabase.3. Command-lineHTTPclientwritteninPythonthatmakesiteasytocomposeand
sendHTTPrequests.
SummaryInthischapter,wedesignedaRESTfulAPItointeractwithasimpleSQLitedatabaseandperformCRUDoperationswithgames.WedefinedtherequirementsforourAPIandweunderstoodthetasksperformedbyeachHTTPmethod.WelearnedtheadvantagesofworkingwithlightweightvirtualenvironmentsinPythonandwesetupavirtualenvironmentwithDjangoRESTFramework.
WecreatedamodeltorepresentandpersistgamesandweexecutedmigrationsinDjango.WelearnedtomanageserializationandserializationofgameinstancesintoJSONrepresentationswithDjangoRESTFramework.WewroteAPIviewstoprocessthedifferentHTTPrequestsandweconfiguredtheURLpatternslisttorouteURLstoviews.
Finally,westartedtheDjangodevelopmentserverandweusedcommand-linetoolstocomposeandsendHTTPrequeststoourRESTfulAPIandanalyzedhoweachHTTPrequestwasprocessedinourcode.WealsoworkedwithGUItoolstocomposeandsendHTTPrequests.
NowthatweunderstandthebasicsofDjangoRESTFramework,wewillexpandthecapabilitiesoftheRESTfulWebAPIbytakingadvantageoftheadvancedfeaturesincludedintheDjangoRESTFramework,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter2.WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjangoInthischapter,wewillexpandthecapabilitiesoftheRESTfulAPIthatwestartedinthepreviouschapter.WewillchangetheORMsettingstoworkwithamorepowerfulPostgreSQLdatabaseandwewilltakeadvantageoftheadvancedfeaturesincludedinDjangoRESTFrameworkthatallowustoreducetheboilerplatecodeforcomplexAPIs,suchasclass-basedviews.Wewill:
UsemodelserializerstoeliminateduplicatecodeWorkwithwrapperstowriteAPIviewsUsethedefaultparsingandrenderingoptionsandmovebeyondJSONBrowsetheAPIDesignaRESTfulAPItointeractwithacomplexPostgreSQLdatabaseUnderstandthetasksperformedbyeachHTTPmethodDeclarerelationshipswiththemodelsManageserializationanddeserializationwithrelationshipsandhyperlinksCreateclassbasedviewsandusegenericclassesWorkwithendpointsfortheAPICreateandretrieverelatedresources
UsingmodelserializerstoeliminateduplicatecodeTheGameSerializerclassdeclaresmanyattributeswiththesamenamesthatweusedintheGamemodelandrepeatsinformation,suchasthetypesandthemax_lengthvalues.TheGameSerializerclassisasubclassofrest_framework.serializers.Serializer,itdeclaresattributesthatwemanuallymappedtotheappropriatetypesandoverridesthecreateandupdatemethods.
Now,wewillcreateanewversionoftheGameSerializerclassthatwillinheritfromtherest_framework.serializers.ModelSerializerclass.TheModelSerializerclassautomaticallypopulatesbothsetofdefaultfieldsandasetofdefaultvalidators.Inaddition,theclassprovidesdefaultimplementationsforthecreateandupdatemethods.
Tip
IncaseyouhaveanyexperiencewithDjangoWebFramework,youwillnoticethattheSerializerandModelSerializerclassesaresimilartotheFormandModelFormclasses.
Now,gotothegamesapi/gamesfolderandopentheserializers.pyfile.Replacethecodeinthisfilewiththefollowingcode,thatdeclaresthenewversionoftheGameSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_01folder:
fromrest_frameworkimportserializers
fromgames.modelsimportGame
classGameSerializer(serializers.ModelSerializer):
classMeta:
model=Game
fields=('id',
'name',
'release_date',
'game_category',
'played')
ThenewGameSerializerclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,theGameclass.Thefieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesthatwewanttoincludeintheserializationfromtherelatedmodel.
Thereisnoneedtooverrideeithercreateorupdatemethodsbecausethegenericbehaviorwillbeenoughinthiscase.TheModelSerializersuperclassprovidesimplementationsforbothmethods.
Wehavereducedtheboilerplatecodethatwedidn'trequireintheGameSerializerclass.We
justneededtospecifythedesiredsetoffieldsinatuple.Now,thetypesrelatedtothegamefieldsareincludedonlyintheGameclass.
Tip
PressCtrl+CtoquitDjango'sdevelopmentserverandexecutethefollowingcommandtostartitagain:
pythonmanage.pyrunserver
WorkingwithwrapperstowriteAPIviewsOurcodeinthegames/views.pyfiledeclaredaJSONResponseclassandtwofunction-basedviews.ThesefunctionsreturnedJSONResponsewhenitwasnecessarytoreturnJSONdataandadjango.Http.Response.HttpResponseinstancewhentheresponsewasjustofanHTTPstatuscode.
NomattertheacceptedcontenttypespecifiedintheHTTPrequestheader,theviewfunctionsalwaysprovidethesamecontentintheresponsebody-JSON.RunthefollowingtwocommandstoretrieveallthegameswithdifferentvaluesfortheAcceptrequestheader-text/htmlandapplication/json:
http:8000/games/Accept:text/html
http:8000/games/Accept:application/json
Thefollowingaretheequivalentcurlcommands:
curl-H'Accept:text/html'-iXGET:8000/games/
curl-H'Accept:application/json'-iXGET:8000/games/
TheprecedingcommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/games/.Thefirstcommanddefinesthetext/htmlvaluefortheAcceptrequestheader.Thesecondcommanddefinestheapplication/jsonvaluefortheAcceptrequestheader.
Youwillnoticethatboththecommandsproducethesameresults,andtherefore,theviewfunctionsdon'ttakeintoaccountthevaluespecifiedfortheAcceptrequestheaderintheHTTPrequests.Theheaderresponseforbothcommandswillincludethefollowingline:
Content-Type:application/json
Thesecondrequestspecifiedthatitwillonlyaccepttext/htmlbuttheresponseincludedaJSONbody,thatis,application/jsoncontent.Thus,ourfirstversionoftheRESTfulAPIisnotpreparedtorendercontentotherfromJSON.WewillmakesomechangestoenabletheAPItorenderothercontents.
WheneverwehavedoubtsaboutthemethodssupportedbyaresourceorresourcecollectioninaRESTfulAPI,wecancomposeandsendanHTTPrequestwiththeOPTIONSHTTPverbandtheURLfortheresourceorresourcecollection.IftheRESTfulAPIimplementstheOPTIONSHTTPverbforaresourceorresourcecollection,itprovidesacomma-separatedlistofHTTPverbsormethodsthatitsupportsasavaluefortheAllowheaderintheresponse.Inaddition,theresponseheaderwillincludeadditionalinformationaboutothersupportedoptions,suchasthecontenttypeitiscapableofparsingfromtherequestandthecontenttypeitiscapableofrenderingontheresponse.
Forexample,ifwewanttoknowtheHTTPverbsthatthegamescollectionsupports,wecan
runthefollowingcommand:
httpOPTIONS:8000/games/
Thefollowingistheequivalentcurlcommand:
curl-iXOPTIONS:8000/games/
ThepreviouscommandwillcomposeandsendthefollowingHTTPrequest:OPTIONShttp://localhost:8000/games/.Therequestwillmatchandruntheviews.game_listfunction,thatis,thegame_listfunctiondeclaredwithinthegames/views.pyfile.Thisfunctiononlyrunsthecodewhentherequest.methodisequalto'GET'or'POST'.Inthiscase,request.methodisequalto'OPTIONS',andtherefore,thefunctionwon'trunanycodeandwon'treturnanyresponse,specifically,itwon'treturnanHttpResponseinstance.Asaresult,wewillseethefollowingInternalServerErrorlistedinDjango'sdevelopmentserverconsoleoutput:
InternalServerError:/games/
Traceback(mostrecentcalllast):
File"/Users/gaston/Projects/PythonRESTfulWebAPI/Django01/lib/python3.5/site-
packages/django/core/handlers/base.py",line158,inget_response
%(callback.__module__,view_name))
ValueError:Theviewgames.views.game_listdidn'treturnanHttpResponseobject.
ItreturnedNoneinstead.
[08/Jun/201620:21:40]"OPTIONS/games/HTTP/1.1"50049173
ThefollowinglinesshowtheheaderfortheoutputthatalsoincludesadetailedHTMLdocumentwithdetailedinformationabouttheerrorbecausethedebugmodeisactivatedforDjango.Wereceivea500InternalServerErrorstatuscode:
HTTP/1.0500InternalServerError
Content-Type:text/html
Date:Wed,08Jun201620:21:40GMT
Server:WSGIServer/0.2CPython/3.5.1
X-Frame-Options:SAMEORIGIN
Obviously,wewanttoprovideamoreconsistentAPIandwewanttoprovideanaccurateresponsewhenwereceivearequestwiththeOPTIONSverbsforeitheragameresourceorthegamescollection.
IfwecomposeandsendanHTTPrequestwiththeOPTIONSverbforagameresource,wewillseethesameerrorandwewillhaveasimilarresponsebecausetheviews.game_detailfunctiononlyrunsthecodewhentherequest.methodisequalto'GET','PUT',or'DELETE'.
Thefollowingcommandswillproducetheexplainederrorwhenwetrytoseetheoptionsofferedforthegameresourcewhoseidorprimarykeyisequalto3.Don'tforgettoreplace3withaprimarykeyvalueofanexistinggameinyourconfiguration:
httpOPTIONS:8000/games/3/
Thefollowingistheequivalentcurlcommand:
curl-iXOPTIONS:8000/games/3/
Wejustneedtomakeafewchangesinthegames/views.pyfiletosolvetheissueswehavebeenanalyzingforourRESTfulAPI.Wewillusethe@api_viewdecorator,declaredinrest_framework.decorators,forourfunction-basedviews.ThisdecoratorallowsustospecifytheHTTPverbsthatourfunctioncanprocess.IftherequestthathastobeprocessedbytheviewfunctionhasanHTTPverbthatisn'tincludedinthestringlistspecifiedasthehttp_method_namesargumentforthe@api_viewdecorator,thedefaultbehaviorreturnsa405MethodNotAllowedstatuscode.Thisway,wemakesurethatwheneverwereceiveanHTTPverbthatisn'tconsideredwithinourfunctionview,wewon'tgenerateanunexpectederrorasthedecoratorhandlestheresponsefortheunsupportedHTTPverbsormethods.
Tip
Underthehoods,the@api_viewdecoratorisawrapperthatconvertsafunction-basedviewsintoasubclassoftherest_framework.views.APIViewclass.ThisclassisthebaseclassforallviewsinDjangoRESTFramework.Aswemightguess,incasewewanttoworkwithclass-basedview,wecancreateclassesthatinheritfromthisclassandwewillhavethesamebenefitsthatweanalyzedforthefunction-basedviewsthatusethedecorator.Wewillworkwithclass-basedviewsintheforthcomingexamples.
Inaddition,aswespecifyastringlistwiththesupportedHTTPverbs,thedecoratorautomaticallybuildstheresponsefortheOPTIONSHTTPverbwiththesupportedmethodsandparserandrendercapabilities.OuractualversionoftheAPIisjustcapableofrenderingJSONasitsoutput.Theusageofthedecoratormakessurethatwealwaysreceiveaninstanceoftherest_framework.request.RequestclassintherequestargumentwhenDjangocallsourviewfunction.ThedecoratoralsohandlestheParserErrorexceptionswhenourfunctionviewsaccesstherequest.dataattributethatmightcauseparsingproblems.
UsingthedefaultparsingandrenderingoptionsandmovebeyondJSONTheAPIViewclassspecifiesdefaultsettingsforeachviewthatwecanoverridebyspecifyingappropriatevaluesinthegamesapi/settings.pyfileorbyoverridingtheclassattributesinsubclasses.Aspreviouslyexplained,theusageoftheAPIViewclassunderthehoodsmakesthedecoratorapplythesedefaultsettings.Thus,wheneverweusethedecorator,thedefaultparserclassesandthedefaultrendererclasseswillbeassociatedwiththefunctionviews.
Bydefault,thevaluefortheDEFAULT_PARSER_CLASSESisthefollowingtupleofclasses:
(
'rest_framework.parsers.JSONParser',
'rest_framework.parsers.FormParser',
'rest_framework.parsers.MultiPartParser'
)
Whenweusethedecorator,theAPIwillbeabletohandleanyofthefollowingcontenttypesthroughtheappropriateparserswhenaccessingtherequest.dataattribute:
application/json
application/x-www-form-urlencoded
multipart/form-data
Tip
Whenweaccesstherequest.dataattributeinthefunctions,DjangoRESTFrameworkexaminesthevaluefortheContent-Typeheaderintheincomingrequestanddeterminestheappropriateparsertoparsetherequestcontent.Ifweusethepreviouslyexplaineddefaultvalues,theDjangoRESTFrameworkwillbeabletoparsethepreviouslylistedcontenttypes.However,itisextremelyimportantthattherequestspecifiestheappropriatevalueintheContent-Typeheader.
Wehavetoremovetheusageoftherest_framework.parsers.JSONParserclassinthefunctionstomakeitpossibletobeabletoworkwithalltheconfiguredparsersandstopworkingwithaparserthatonlyworkswithJSON.Thegame_listfunctionexecutesthefollowingtwolineswhenrequest.methodisequalto'POST':
game_data=JSONParser().parse(request)
game_serializer=GameSerializer(data=game_data)
WewillremovethefirstlinethatusestheJSONParserandwewillpassrequest.dataasthedataargumentfortheGameSerializer.Thefollowinglinewillreplacethepreviouslines:
game_serializer=GameSerializer(data=request.data)
Thegame_detailfunctionexecutesthefollowingtwolineswhenrequest.methodisequalto
'PUT':
game_data=JSONParser().parse(request)
game_serializer=GameSerializer(game,data=game_data)
Wewillmakethesameeditsdoneforthecodeinthegame_listfunction.WewillremovethefirstlinethatusestheJSONParserandwewillpassrequest.dataasthedataargumentfortheGameSerializer.Thefollowinglinewillreplacethepreviouslines:
game_serializer=GameSerializer(game,data=request.data)
Bydefault,thevaluefortheDEFAULT_RENDERER_CLASSESisthefollowingtupleofclasses:
(
'rest_framework.renderers.JSONRenderer',
'rest_framework.renderers.BrowsableAPIRenderer',
)
Whenweusethedecorator,theAPIwillbeabletorenderthefollowingcontenttypesintheresponse,throughtheappropriaterenderers,whenworkingwiththerest_framework.response.Responseobject:
application/json
text/html
Bydefault,thevaluefortheDEFAULT_CONTENT_NEGOTIATION_CLASSistherest_framework.negotiation.DefaultContentNegotiationclass.Whenweusethedecorator,theAPIwillusethiscontentnegotiationclasstoselecttheappropriaterendererfortheresponsebasedontheincomingrequest.Thisway,whenarequestspecifiesthatitwillaccepttext/html,thecontentnegotiationclassselectstherest_framework.renderers.BrowsableAPIRenderertorendertheresponseandgeneratetext/htmlinsteadofapplication/json.
WehavetoreplacetheusageofboththeJSONResponseandHttpResponseclassesinthefunctionswiththerest_framework.response.Responseclass.TheResponseclassusesthepreviouslyexplainedcontentnegotiationfeatures,rendersthereceiveddataintotheappropriatecontenttype,andreturnsittotheclient.
Now,gotothegamesapi/gamesfolderandopentheviews.pyfile.ReplacethecodeinthisfilewiththefollowingcodethatremovestheJSONResponseclassandusesthe@api_viewdecoratorforthefunctionsandtherest_framework.response.Responseclass.Themodifiedlinesarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_02_02folder:
fromrest_framework.parsersimportJSONParser
fromrest_frameworkimportstatus
fromrest_framework.decoratorsimportapi_view
fromrest_framework.responseimportResponse
fromgames.modelsimportGame
fromgames.serializersimportGameSerializer
@api_view(['GET','POST'])
defgame_list(request):
ifrequest.method=='GET':
games=Game.objects.all()
games_serializer=GameSerializer(games,many=True)
returnResponse(games_serializer.data)
elifrequest.method=='POST':
game_serializer=GameSerializer(data=request.data)
ifgame_serializer.is_valid():
game_serializer.save()
returnResponse(game_serializer.data,
status=status.HTTP_201_CREATED)
returnResponse(game_serializer.errors,status=status.HTTP_400_BAD_REQUEST)
@api_view(['GET','PUT','POST'])
defgame_detail(request,pk):
try:
game=Game.objects.get(pk=pk)
exceptGame.DoesNotExist:
returnResponse(status=status.HTTP_404_NOT_FOUND)
ifrequest.method=='GET':
game_serializer=GameSerializer(game)
returnResponse(game_serializer.data)
elifrequest.method=='PUT':
game_serializer=GameSerializer(game,data=request.data)
ifgame_serializer.is_valid():
game_serializer.save()
returnResponse(game_serializer.data)
returnResponse(game_serializer.errors,
status=status.HTTP_400_BAD_REQUEST)
elifrequest.method=='DELETE':
game.delete()
returnResponse(status=status.HTTP_204_NO_CONTENT)
Afteryousavetheprecedingchanges,runthefollowingcommand:
httpOPTIONS:8000/games/
Thefollowingistheequivalentcurlcommand:
curl-iXOPTIONS:8000/games/
ThepreviouscommandwillcomposeandsendthefollowingHTTPrequest:OPTIONShttp://localhost:8000/games/.Therequestwillmatchandruntheviews.game_listfunction,thatis,thegame_listfunctiondeclaredwithinthegames/views.pyfile.Weaddedthe
@api_viewdecoratortothisfunction,andtherefore,itisnowcapableofdeterminingthesupportedHTTPverbs,parsing,andrenderingcapabilities.Thefollowinglinesshowtheoutput:
HTTP/1.0200OK
Allow:GET,POST,OPTIONS
Content-Type:application/json
Date:Thu,09Jun201620:24:31GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"description":"",
"name":"GameList",
"parses":[
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"renders":[
"application/json",
"text/html"
]
}
TheresponseheaderincludesanAllowkeywithacomma-separatedlistofHTTPverbssupportedbytheresourcecollectionasitsvalue:GET,POST,OPTIONS.Asourrequestdidn'tspecifytheallowedcontenttype,thefunctionrenderedtheresponsewiththedefaultapplication/jsoncontenttype.TheresponsebodyspecifiestheContent-typethattheresourcecollectionparsesandtheContent-typethatitrenders.
RunthefollowingcommandtocomposeandsendanHTTPrequestwiththeOPTIONSverbforagameresource.Don'tforgettoreplace3withaprimarykeyvalueofanexistinggameinyourconfiguration.
httpOPTIONS:8000/games/3/
Thefollowingistheequivalentcurlcommand:
curl-iXOPTIONS:8000/games/3/
TheprecedingcommandwillcomposeandsendthefollowingHTTPrequest:OPTIONShttp://localhost:8000/games/3/.Therequestwillmatchandruntheviews.game_detailfunction,thatis,thegame_detailfunctiondeclaredwithinthegames/views.pyfile.Wealsoaddedthe@api_viewdecoratortothisfunction,andtherefore,itiscapableofdeterminingthesupportedHTTPverbs,parsing,andrenderingcapabilities.Thefollowinglinesshowtheoutput:
HTTP/1.0200OK
Allow:GET,POST,OPTIONS,PUT
Content-Type:application/json
Date:Thu,09Jun201621:35:58GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"description":"",
"name":"GameDetail",
"parses":[
"application/json",
"application/x-www-form-urlencoded",
"multipart/form-data"
],
"renders":[
"application/json",
"text/html"
]
}
TheresponseheaderincludesanAllowkeywithacomma-separatedlistofHTTPverbssupportedbytheresourceasitsvalue:GET,POST,OPTIONS,PUT.Theresponsebodyspecifiesthecontent-typethattheresourceparsesandthecontent-typethatitrenders,withthesamecontentsreceivedinthepreviousOPTIONSrequestappliedtoaresourcecollection,thatis,toagamescollection.
InChapter1,DevelopingRESTfulAPIswithDjango,whenwecomposedandsentPOSTandPUTcommands,wehadtousetheusethe-H"Content-Type:application/json"optiontotellcurltosendthedataspecifiedafterthe-doptionasapplication/jsoninsteadofthedefaultapplication/x-www-form-urlencoded.Now,inadditiontoapplication/json,ourAPIiscapableofparsingapplication/x-www-form-urlencodedandmultipart/form-datadataspecifiedinthePOSTandPUTrequests.Thus,wecancomposeandsendaPOSTcommandthatsendsthedataasapplication/x-www-form-urlencoded,withthechangesmadetoourAPI.
WewillcomposeandsendanHTTPrequesttocreateanewgame.Inthiscase,wewillusethe-foptionforHTTPie,thatserializesdataitemsfromthecommandlineasformfieldsandsetstheContent-Typeheaderkeytotheapplication/x-www-form-urlencodedvalue:
http-fPOST:8000/games/name='ToyStory4'game_category='3DRPG'
played=falserelease_date='2016-05-18T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand.Notethatwedon'tusethe-Hoptionandcurlwillsendthedatainthedefaultapplication/x-www-form-urlencoded:
curl-iXPOST-d'{"name":"ToyStory4","game_category":"3DRPG","played":
"false","release_date":"2016-05-18T03:02:00.776594Z"}':8000/games/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:POSThttp://localhost:8000/games/withtheContent-Typeheaderkeysettotheapplication/x-www-form-urlencodedvalueandthefollowingdata:
name=Toy+Story+4&game_category=3D+RPG&played=false&release_date=2016-05-
18T03%3A02%3A00.776594Z
Therequestspecifies/games/,andtherefore,itwillmatch'^games/$'andruntheviews.game_listfunction,thatis,theupdatedgame_detailfunctiondeclaredwithinthegames/views.pyfile.AstheHTTPverbfortherequestisPOST,therequest.methodpropertyisequalto'POST',andtherefore,thefunctionwillexecutethecodethatcreatesaGameSerializerinstanceandpassesrequest.dataasthedataargumentforitscreation.Therest_framework.parsers.FormParserclasswillparsethedatareceivedintherequest,thecodecreatesanewGameand,ifthedataisvalid,itsavesthenewGame.IfthenewGamewassuccessfullypersistedinthedatabase,thefunctionreturnsanHTTP201CreatedstatuscodeandtherecentlypersistedGameserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewGameobjectintheJSONresponse:
HTTP/1.0201Created
Allow:OPTIONS,POST,GET
Content-Type:application/json
Date:Fri,10Jun201620:38:40GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"game_category":"3DRPG",
"id":20,
"name":"ToyStory4",
"played":false,
"release_date":"2016-05-18T03:02:00.776594Z"
}
Wecanrunthefollowingcommandafterwemakethechangesinthecode,toseewhathappenswhenwecomposeandsendanHTTPrequestwithanHTTPverbthatisnotsupported:
httpPUT:8000/games/
Thefollowingistheequivalentcurlcommand:
curl-iXPUT:8000/games/
ThepreviouscommandwillcomposeandsendthefollowingHTTPrequest:PUThttp://localhost:8000/games/.Therequestwillmatchandtrytoruntheviews.game_listfunction,thatis,thegame_listfunctiondeclaredwithinthegames/views.pyfile.The@api_viewdecoratorweaddedtothisfunctiondoesn'tinclude'PUT'inthestringlistwiththeallowedHTTPverbs,andtherefore,thedefaultbehaviorreturnsa405MethodNotAllowedstatuscode.Thefollowinglinesshowtheoutputalongwiththeresponsefromthepreviousrequest.AJSONcontentprovidesadetailkeywithastringvalue,whichindicatesthatthePUTmethodisnotallowed:
HTTP/1.0405MethodNotAllowed
Allow:GET,OPTIONS,POST
Content-Type:application/json
Date:Sat,11Jun201600:49:30GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"detail":"Method"PUT"notallowed."
}
BrowsingtheAPIWiththerecentedits,wemadeitpossibleforourAPItousethedefaultcontentrenderersconfiguredinDjangoRESTFramework,andtherefore,ourAPIiscapableofrenderingthetext/htmlcontent.WecantakeadvantageofthebrowsableAPI,afeatureincludedinDjangoRESTFrameworkthatgenerateshuman-friendlyHTMLoutputforeachresourcewhenevertherequestspecifiestext/htmlasthevaluefortheContent-typekeyintherequestheader.
WheneverweenteraURLforanAPIresourceinawebbrowser,thebrowserwillrequireanHTMLresponse,andtherefore,DjangoRESTFrameworkwillprovideanHTMLresponsebuiltwithBootstrap(http://getbootstrap.com).ThisresponsewillincludeasectionthatdisplaystheresourcecontentinJSON,buttonstoperformdifferentrequests,andformstosubmitdatatotheresources.AseverythinginDjangoRESTFramework,wecancustomizethetemplatesandthemesusedtogeneratethebrowsableAPI.
Openawebbrowserandenterhttp://localhost:8000/games/.ThebrowsableAPIwillcomposeandsendaGETrequestto/games/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONgameslist.ThefollowingscreenshotshowstherenderedwebpageafterenteringtheURLinawebbrowserwiththeresourcedescription-GameList:
Tip
IfyoudecidetobrowsetheAPIinawebbrowserrunningonanothercomputerordeviceconnectedtotheLAN,rememberthatyouhavetousethedevelopmentcomputer'sassignedIPaddressinsteadoflocalhost.Forexample,ifthecomputer'sassignedIPv4IPaddressis192.168.1.106,insteadofhttp://localhost:8000/games/,youshouldusehttp://192.168.1.106:8000/games/.Ofcourse,youcanalsousethehostnameinsteadoftheIPaddress.
ThebrowsableAPIusestheinformationabouttheallowedmethodsforaresourcetoprovideuswithbuttonstorunthesemethods.Attheright-handsideoftheresourcedescription,thebrowsableAPIshowsanOPTIONSbuttonandaGET drop-downbutton.TheOPTIONSbuttonallowsustomakeanOPTIONSrequestto/games/,thatis,tothecurrentresource.TheGET drop-downbuttonallowsustomakeaGETrequestto/games/again.Ifweclickonortapthedownarrow,wecanselectthejsonoptionandthebrowsableAPIwilldisplaytherawJSONresultofaGETrequestto/games/withouttheheaders.
Atthebottomoftherenderedwebpage,thebrowsableAPIprovidesussomecontroltogenerateaPOSTrequestto/games/.TheMediatypedropdownallowsustoselectbetweentheconfiguredsupportedparsersforourAPI:
application/json
application/x-www-form-urlencoded
multipart/form-data
TheContenttextboxallowsustospecifythedatatobesenttothePOSTrequestformattedasspecifiedintheMediatypedropdown.Selectapplication/jsonintheMediatypedropdownandenterthefollowingJSONcontentintheContenttextbox:
{
"name":"Chuzzle2",
"release_date":"2016-05-18T03:02:00.776594Z",
"game_category":"2Dmobile",
"played":false
}
ClickortaponPOST.ThebrowsableAPIwillcomposeandsendaPOSTrequestto/games/withthepreviouslyspecifieddataasJSON,andwewillseetheresultsofthecallinthewebbrowser.
ThefollowingscreenshotshowsawebbrowserdisplayingtheHTTPstatuscode201CreatedintheresponseandthepreviouslyexplaineddropdownandtextboxwiththePOSTbuttontoallowustocontinuecomposingandsendingPOSTrequeststo/games/:
Now,entertheURLforanexistinggameresource,suchashttp://localhost:8000/games/2/.Makesureyoureplace2withtheidorprimarykeyofanexistinggameinthepreviouslyrenderedGamesList.ThebrowsableAPIwillcomposeandsendaGETrequestto/games/2/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONdataforthegame.
ThefollowingscreenshotshowstherenderedwebpageafterenteringtheURLinawebbrowserwiththeresourcedescription-GameDetail:
Tip
ThebrowsableAPIfeatureallowsustoeasilycheckhowtheAPIworksandtocomposeandsendHTTPrequestswithdifferentmethodstoanywebbrowserthathasaccesstoourLAN.WewilltakeadvantageoftheadditionalfeaturesincludedinthebrowsableAPI,suchasHTMLformsthatallowustoeasilycreatenewresources,later,afterwebuildanewRESTfulAPIwithPythonandDjangoRESTFramework.
DesigningaRESTfulAPItointeractwithacomplexPostgreSQLdatabaseSofar,ourRESTfulAPIhasperformedCRUDoperationsonasingledatabasetable.Now,wewanttocreateamorecomplexRESTfulAPIwithDjangoRESTFrameworktointeractwithacomplexdatabasemodelthathastoallowustoregisterplayerscoresforplayedgamesthataregroupedintogamecategories.InourpreviousRESTfulAPI,weusedastringfieldtospecifythegamecategoryforagame.Inthiscase,wewanttobeabletoeasilyretrieveallthegamesthatbelongtoaspecificgamecategory,andtherefore,wewillhavearelationshipbetweenagameandagamecategory.
WeshouldbeabletoperformCRUDoperationsondifferentrelatedresourcesandresourcecollections.ThefollowinglistenumeratestheresourcesandthemodelnamesthatwewillusetorepresenttheminDjangoRESTFramework:
Gamecategories(GameCategorymodel)Games(Gamemodel)Players(Playermodel)Playerscores(PlayerScoremodel)
Thegamecategory(GameCategory)justrequiresaname,andweneedthefollowingdataforagame(Game):
Aforeignkeytoagamecategory(GameCategory)AnameAreleasedateAboolvalueindicatingwhetherthegamewasplayedatleastoncebyaplayerornotAtimestampwiththedateandtimeinwhichthegamewasinsertedinthedatabase
Weneedthefollowingdataforaplayer(Player):
AgendervalueAnameAtimestampwiththedateandtimeinwhichtheplayerwasinsertedinthedatabase
Weneedthefollowingdataforthescoreachievedbyaplayer(PlayerScore):
Aforeignkeytoaplayer(Player)Aforeignkeytoagame(Game)AscorevalueAdateinwhichthescorevaluewasachievedbytheplayer
Tip
WewilltakeadvantageofalltheresourcesandtheirrelationshipstoanalyzedifferentoptionsthatDjangoRESTFrameworkprovidesuswhenworkingwithrelatedresources.Insteadof
buildinganAPIthatusesthesameconfigurationtodisplayrelatedresources,wewillusediverseconfigurationsthatwillallowustoselectthemostappropriateoptionsbasedontheparticularrequirementsoftheAPIsthatwearedeveloping.
UnderstandingthetasksperformedbyeachHTTPmethodThefollowingtableshowstheHTTPverbs,thescope,andthesemanticsforthemethodsthatournewAPImustsupport.EachmethodiscomposedbyanHTTPverbandascopeandallthemethodshavewell-definedmeaningsforalltheresourcesandcollections.
HTTPverb Scope Semantics
GET
Collectionofgamecategories
Retrieveallthestoredgamecategoriesinthecollection,sortedbytheirnameinascendingorder.EachgamecategorymustincludealistofURLsforeachgameresourcethatbelongstothecategory.
GETGamecategory
Retrieveasinglegamecategory.ThegamecategorymustincludealistofURLsforeachgameresourcethatbelongstothecategory.
POST
Collectionofgamecategories
Createanewgamecategoryinthecollection.
PUTGamecategory Updateanexistinggamecategory.
PATCHGamecategory Updateoneormorefieldsofanexistinggamecategory.
DELETEGamecategory Deleteanexistinggamecategory.
GETCollectionofgames
Retrieveallthestoredgamesinthecollection,sortedbytheirnameinascendingorder.Eachgamemustincludeitsgamecategorydescription.
GET Game Retrieveasinglegame.Thegamemustincludeitsgamecategorydescription.
POST Collectionofgames
Createanewgameinthecollection.
PUTGamecategory Updateanexistinggame.
PATCHGamecategory Updateoneormorefieldsofanexistinggame.
DELETEGamecategory Deleteanexistinggame.
GETCollectionofplayers
Retrieveallthestoredplayersinthecollection,sortedbytheirnameinascendingorder.Eachplayermustincludealistoftheregisteredscores,sortedbyscoreindescendingorder.Thelistmustincludeallthedetailsforthescoreachievedbytheplayeranditsrelatedgame.
GET PlayerRetrieveasingleplayer.Theplayermustincludealistoftheregisteredscores,sortedbyscoreindescendingorder.Thelistmustincludeallthedetailsforthescoreachievedbytheplayeranditsrelatedgame.
POSTCollectionofplayers Createanewplayerinthecollection.
PUT Player Updateanexistingplayer.
PATCH Player Updateoneormorefieldsofanexistingplayer.
DELETE Player Deleteanexistingplayer.
GETCollectionofscores
Retrieveallthestoredscoresinthecollection,sortedbyscoreindescendingorder.Eachscoremustincludetheplayer'snamethatachievedthescoreandthegame'sname.
GET ScoreRetrieveasinglescore.Thescoremustincludetheplayer'snamethatachievedthescoreandthegame'sname.
POST Collectionofscores
Createanewscoreinthecollection.Thescoremustberelatedtoanexistingplayerandanexistinggame.
PUT Score Updateanexistingscore.
PATCH Score Updateoneormorefieldsofanexistingscore.
DELETE Score Deleteanexistingscore.
WewantourAPItobeabletoupdateasinglefieldforanexistingresource,andtherefore,wewillprovideanimplementationforthePATCHmethod.ThePUTmethodismeanttoreplaceanentireresourceandthePATCHmethodismeanttoapplyadeltatoanexistingresource.Inaddition,ourRESTfulAPImustsupporttheOPTIONSmethodforalltheresourcesandcollectionofresources.
Wedon'twanttospendtimechoosingandconfiguringthemostappropriateORM,asseeninourpreviousAPI;wejustwanttofinishtheRESTfulAPIassoonaspossibletostartinteractingwithit.WewilluseallthefeaturesandreusableelementsincludedinDjangoRESTFrameworktomakeiteasytobuildourAPI.WewillworkwithaPostgreSQLdatabase.However,incaseyoudon'twanttospendtimeinstallingPostgreSQL,youcanskipthechangeswemakeinDjangoRESTFrameworkORMconfigurationandcontinueworkingwiththedefaultSQLitedatabase.
Intheprecedingtable,wehaveahugenumberofmethodsandscopes.ThefollowinglistenumeratestheURIsforeachscopementionedinthetable,where{id}hastobereplacedwiththenumericidortheprimarykeyoftheresource:
Collectionofgamecategories:/game-categories/Gamecategory:/game-category/{id}/Collectionofgames:/games/Game:/game/{id}/Collectionofplayers:/players/Player:/player/{id}/Collectionofscores:/player-scores/Score:/player-score/{id}/
Let'sconsiderthathttp://localhost:8000/istheURLfortheAPIrunningontheDjangodevelopmentserver.WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:8000/game-categories/)toretrieveallthestoredgamecategoriesinthecollection:
GEThttp://localhost:8000/game-categories/
DeclaringrelationshipswiththemodelsMakesureyouquittheDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+Cintheterminalorcommand-promptwindowinwhichitisrunning.Now,wewillcreatethemodelsthatwearegoingtousetorepresentandpersistthegamecategories,games,playersandscores,andtheirrelationships.Openthegames/models.pyfileandreplaceitscontentswiththefollowingcode.Thelinesthatdeclarefieldsrelatedtoothermodelsarehighlightedinthecodelisting.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder.
fromdjango.dbimportmodels
classGameCategory(models.Model):
name=models.CharField(max_length=200)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
classGame(models.Model):
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=200)
game_category=models.ForeignKey(
GameCategory,
related_name='games',
on_delete=models.CASCADE)
release_date=models.DateTimeField()
played=models.BooleanField(default=False)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
classPlayer(models.Model):
MALE='M'
FEMALE='F'
GENDER_CHOICES=(
(MALE,'Male'),
(FEMALE,'Female'),
)
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=50,blank=False,default='')
gender=models.CharField(
max_length=2,
choices=GENDER_CHOICES,
default=MALE,
)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
classPlayerScore(models.Model):
player=models.ForeignKey(
Player,
related_name='scores',
on_delete=models.CASCADE)
game=models.ForeignKey(
Game,
on_delete=models.CASCADE)
score=models.IntegerField()
score_date=models.DateTimeField()
classMeta:
#Orderbyscoredescending
ordering=('-score',)
Theprecedingcodedeclaresthefollowingfourmodels,specificallyfourclassesassubclassesofthedjango.db.models.Modelclass:
GameCategory
Game
Player
PlayerScore
Djangoautomaticallyaddsanauto-incrementintegerprimarykeycolumnnamedidwhenitcreatesthedatabasetablerelatedtoeachmodel.Wespecifiedthefieldtypes,maximumlengths,anddefaultsformanyattributes.EachclassdeclaresaMetainnerclassthatdeclaresanorderingattribute.TheMetainnerclassdeclaredwithinthePlayerScoreclassspecifies'-score'asthevalueoftheorderingtuple,withadashasaprefixofthefieldnameandorderedbyscoreindescendingorder,insteadofthedefaultascendingorder.
TheGameCategory,Game,andPlayerclassesdeclarethe__str__methodthatreturnsthecontentsofthenameattributethatprovidesthenameortitleforeachofthesemodels.So,Djangowillcallthismethodwheneverithastoprovideahuman-readablerepresentationforthemodel.
TheGamemodeldeclaresthegame_categoryfieldwiththefollowingline:
game_category=models.ForeignKey(
GameCategory,
related_name='games',
on_delete=models.CASCADE)
Theprecedinglineusesthedjango.db.models.ForeignKeyclasstoprovideamany-to-onerelationshiptotheGameCategorymodel.The'games'valuespecifiedfortherelated_nameargumentcreatesabackwardsrelationfromtheGameCategorymodeltotheGamemodel.ThisvalueindicatesthenametobeusedfortherelationfromtherelatedGameCategoryobjectbacktoaGameobject.Now,wewillbeabletoaccessallthegamesthatbelongtoaspecificgamecategory.Wheneverwedeleteagamecategory,wewantallthegamesthatbelongtothiscategorytobedeletedtoo,andtherefore,wespecifiedthemodels.CASCADEvaluefortheon_deleteargument.
ThePlayerScoremodeldeclarestheplayerfieldwiththefollowingline:
player=models.ForeignKey(
Player,
related_name='scores',
on_delete=models.CASCADE)
Theprecedinglineusesthedjango.db.models.ForeignKeyclasstoprovideamany-to-onerelationshiptothePlayermodel.The'scores'valuespecifiedfortherelated_nameargumentcreatesabackwardsrelationfromthePlayermodeltothePlayerScoremodel.ThisvalueindicatesthenametobeusedfortherelationfromtherelatedPlayerobjectbacktoaPlayerScoreobject.Now,wewillbeabletoaccessallthescoresarchivebyaspecificplayer.Wheneverwedeleteaplayer,wewantallthescoresachievedbythisplayertobedeletedtoo,andtherefore,wespecifiedthemodels.CASCADEvaluefortheon_deleteargument.
ThePlayerScoremodeldeclaresthegamefieldwiththefollowingline:
game=models.ForeignKey(
Game,
on_delete=models.CASCADE)
Theprecedinglineusesthedjango.db.models.ForeignKeyclasstoprovideamany-to-onerelationshiptotheGamemodel.Inthiscase,wedon'tcreateabackwardsrelationbecausewedon'tneedit.Thus,wedon'tspecifyavaluefortherelated_nameargument.Wheneverwedeleteagame,wewantalltheregisteredscoresforthisgametobedeletedtoo,andtherefore,wespecifiedthemodels.CASCADEvaluefortheon_deleteargument.
Incaseyoucreatedanewvirtualenvironmenttoworkwiththisexampleoryoudownloadedthesamplecodeforthebook,youdon'tneedtodeleteanyexistingdatabase.However,incaseyouaremakingchangestothecodeforourpreviousAPIexample,youhavetodeletethegamesapi/db.sqlite3fileandthegames/migrationsfolder.
Then,itisnecessarytocreatetheinitialmigrationforthenewmodelswerecentlycoded.WejustneedtorunthefollowingPythonscriptsandwewillalsosynchronizethedatabaseforthefirsttime.AswelearnedfromourpreviousexampleAPI,bydefault,DjangousesanSQLitedatabase.Inthisexample,wewillbeworkingwithaPostgreSQLdatabase.However,incaseyouwanttouseSQLite,youcanskipthestepsrelatedtoPostgreSQL,itsconfigurationinDjango,andjumptothemigrationsgenerationcommand.
YouwillhavetodownloadandinstallaPostgreSQLdatabaseincaseyouaren'talreadyrunningitinyourcomputerorinadevelopmentserver.Youcandownloadandinstallthisdatabasemanagementsystemfromitswebpage-http://www.postgresql.org.IncaseyouareworkingwithmacOS,Postgres.appprovidesaneasywaytoinstallandusePostgreSQLonthisoperatingsystem-http://postgresapp.com.
Tip
YouhavetomakesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable.Youshouldbeabletoexecutethepsqlcommand-lineutilityfromyourcurrentterminalorcommandprompt.Incasethefolderisn'tincludedinthePATH,youwillreceiveanerrorindicatingthatthepg_configfilecannotbefoundwhentryingtoinstallthepsycopg2package.Inaddition,youwillhavetousethefullpathtoeachofthePostgreSQLcommand-linetoolswewilluseinthesubsequentsteps.
WewillusethePostgreSQLcommand-linetoolstocreateanewdatabasenamedgames.IncaseyoualreadyhaveaPostgreSQLdatabasewiththisname,makesurethatyouuseanothernameinallthecommandsandconfigurations.YoucanperformthesametaskwithanyPostgreSQLGUItool.IncaseyouaredevelopingonLinux,itisnecessarytorunthecommandsasthepostgresuser.RunthefollowingcommandinmacOSorWindowstocreateanewdatabasenamedgames.Notethatthecommandwon'tproduceanyoutput:
createdbgames
InLinux,runthefollowingcommandtousethepostgresuser:
sudo-upostgrescreatedbgames
Now,wewillusethepsqlcommand-linetooltorunsomeSQLstatementstocreateaspecificuserthatwewilluseinDjangoandassignthenecessaryrolesforit.InmacOSorWindows,runthefollowingcommandtolaunchpsql:
psql
InmacOS,youmightneedtorunthefollowingcommandtolaunchpsqlwiththepostgresincasethepreviouscommanddoesn'twork,asitwilldependonthewayinwhichyouinstalledPostgreSQL:
sudo-upostgrespsql
InLinux,runthefollowingcommandtousethepostgresuser.
sudo-upsql
Then,runthefollowingSQLstatementsandfinallyenter\qtoexitthepsqlcommand-linetool.Replaceuser_namewithyourdesiredusernametouseinthenewdatabaseandpasswordwithyourchosenpassword.WewillusetheusernameandpasswordintheDjangoconfiguration.Youdon'tneedtorunthestepsifyouarealreadyworkingwithaspecificuser
inPostgreSQLandyouhavealreadygrantedprivilegestothedatabasefortheuser:
CREATEROLEuser_nameWITHLOGINPASSWORD'password';
GRANTALLPRIVILEGESONDATABASEgamesTOuser_name;
ALTERUSERuser_nameCREATEDB;
\q
ThedefaultSQLitedatabaseengineandthedatabasefilenamearespecifiedinthegamesapi/settings.pyPythonfile.IncaseyoudecidetoworkwithPostgreSQLinsteadofSQLiteforthisexample,replacethedeclarationoftheDATABASESdictionarywiththefollowinglines.Thenesteddictionarymapsthedatabasenameddefaultwiththedjango.db.backends.postgresqldatabaseengine,thedesireddatabasename,anditssettings.Inthiscase,wewillcreateadatabasenamedgames.Makesureyouspecifythedesireddatabasenameinthevalueforthe'NAME'keyandthatyouconfiguretheuser,password,host,andportbasedonyourPostgreSQLconfiguration.Incaseyoufollowedtheprevioussteps,usethesettingsspecifiedinthesesteps:
DATABASES={
'default':{
'ENGINE':'django.db.backends.postgresql',
#Replacegameswithyourdesireddatabasename
'NAME':'games',
#Replaceusernamewithyourdesiredusername
'USER':'user_name',
#Replacepasswordwithyourdesiredpassword
'PASSWORD':'password',
#Replace127.0.0.1withthePostgreSQLhost
'HOST':'127.0.0.1',
#Replace5432withthePostgreSQLconfiguredport
#incaseyouaren'tusingthedefaultport
'PORT':'5432',
}
}
IncaseyoudecidedtousePostgreSQL,aftermakingtheprecedingchanges,itisnecessarytoinstallthePsycopg2package(psycopg2).ThispackageisaPython-PostgreSQLDatabaseAdapterandDjangousesittointeractwithaPostgreSQLdatabase.
InmacOSinstallations,wehavetomakesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable.Forexample,incasethepathtothebinfolderis/Applications/Postgres.app/Contents/Versions/latest/bin,wemustexecutethefollowingcommandtoaddthisfoldertothePATHenvironmentalvariable:
exportPATH=$PATH:/Applications/Postgres.app/Contents/Versions/latest/bin
OncewehavemadesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable,wejustneedtorunthefollowingcommandtoinstallthispackage:
pipinstallpsycopg2
Thelastlinesoftheoutputwillindicatethatthepsycopg2packagehasbeensuccessfully
installed:
Collectingpsycopg2
Installingcollectedpackages:psycopg2
Runningsetup.pyinstallforpsycopg2
Successfullyinstalledpsycopg2-2.6.2
Now,runthefollowingPythonscripttogeneratethemigrationsthatwillallowustosynchronizethedatabaseforthefirsttime:
pythonmanage.pymakemigrationsgames
Thefollowinglinesshowtheoutputgeneratedafterrunningthepreviouscommand:
Migrationsfor'games':
0001_initial.py:
-CreatemodelGame
-CreatemodelGameCategory
-CreatemodelPlayer
-CreatemodelPlayerScore
-Addfieldgame_categorytogame
Theoutputindicatesthatthegamesapi/games/migrations/0001_initial.pyfileincludesthecodetocreatetheGame,GameCategory,Player,andPlayerScoremodels.ThefollowinglinesshowthecodeforthisfilethatwasautomaticallygeneratedbyDjango.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
#-*-coding:utf-8-*-
#GeneratedbyDjango1.9.7on2016-06-1720:39
from__future__importunicode_literals
fromdjango.dbimportmigrations,models
importdjango.db.models.deletion
classMigration(migrations.Migration):
initial=True
dependencies=[
]
operations=[
migrations.CreateModel(
name='Game',
fields=[
('id',models.AutoField(auto_created=True,primary_key=True,
serialize=False,verbose_name='ID')),
('created',models.DateTimeField(auto_now_add=True)),
('name',models.CharField(max_length=200)),
('release_date',models.DateTimeField()),
('played',models.BooleanField(default=False)),
],
options={
'ordering':('name',),
},
),
migrations.CreateModel(
name='GameCategory',
fields=[
('id',models.AutoField(auto_created=True,primary_key=True,
serialize=False,verbose_name='ID')),
('name',models.CharField(max_length=200)),
],
options={
'ordering':('name',),
},
),
migrations.CreateModel(
name='Player',
fields=[
('id',models.AutoField(auto_created=True,primary_key=True,
serialize=False,verbose_name='ID')),
('created',models.DateTimeField(auto_now_add=True)),
('name',models.CharField(default='',max_length=50)),
('gender',models.CharField(choices=[('M','Male'),('F',
'Female')],default='M',max_length=2)),
],
options={
'ordering':('name',),
},
),
migrations.CreateModel(
name='PlayerScore',
fields=[
('id',models.AutoField(auto_created=True,primary_key=True,
serialize=False,verbose_name='ID')),
('score',models.IntegerField()),
('score_date',models.DateTimeField()),
('game',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,to='games.Game')),
('player',
models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='scores',to='games.Player')),
],
options={
'ordering':('-score',),
},
),
migrations.AddField(
model_name='game',
name='game_category',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE,
related_name='games',to='games.GameCategory'),
),
]
Theprecedingcodedefinesasubclassofthedjango.db.migrations.MigrationclassnamedMigrationthatdefinesanoperationslistwithmanymigrations.CreateModel.Each
migrations.CreateModelwillcreatethetableforeachoftherelatedmodels.NotethatDjangohasautomaticallyaddedanidfieldforeachofthemodels.Theoperationsareexecutedinthesameorderinwhichtheyappearinthelist.ThecodecreatesGame,GameCategory,Player,PlayerScore,andfinallyaddsthegame_categoryfieldtoGamewiththeforeignkeytoGameCategorybecauseitcreatedtheGamemodelbeforetheGameCategorymodel.ThecodecreatestheforeignkeysforPlayerScorewhenitcreatesthemodel:
Now,runthefollowingPythonscripttoapplyallthegeneratedmigrations.
pythonmanage.pymigrate
Thefollowinglinesshowtheoutputgeneratedafterrunningthepreviouscommand:
Operationstoperform:
Applyallmigrations:sessions,contenttypes,games,admin,auth
Runningmigrations:
Renderingmodelstates...DONE
Applyingcontenttypes.0001_initial...OK
Applyingauth.0001_initial...OK
Applyingadmin.0001_initial...OK
Applyingadmin.0002_logentry_remove_auto_add...OK
Applyingcontenttypes.0002_remove_content_type_name...OK
Applyingauth.0002_alter_permission_name_max_length...OK
Applyingauth.0003_alter_user_email_max_length...OK
Applyingauth.0004_alter_user_username_opts...OK
Applyingauth.0005_alter_user_last_login_null...OK
Applyingauth.0006_require_contenttypes_0002...OK
Applyingauth.0007_alter_validators_add_error_messages...OK
Applyinggames.0001_initial...OK
Applyingsessions.0001_initial...OK
Afterwerunthepreviouscommand,wecanusethePostgreSQLcommandlineoranyotherapplicationthatallowsustoeasilycheckthecontentsofthePostreSQLdatabasetocheckthetablesthatDjangogenerated.IncaseyouareworkingwithSQLite,wehavealreadylearnedhowtocheckthetablesinChapter1,DevelopingRESTfulAPIswithDjango.
Runthefollowingcommandtolistthegeneratedtables:
psql--username=user_name--dbname=games--command="\dt"
Thefollowinglinesshowtheoutputwithallthegeneratedtablenames:
Listofrelations
Schema|Name|Type|Owner
--------+----------------------------+-------+-----------
public|auth_group|table|user_name
public|auth_group_permissions|table|user_name
public|auth_permission|table|user_name
public|auth_user|table|user_name
public|auth_user_groups|table|user_name
public|auth_user_user_permissions|table|user_name
public|django_admin_log|table|user_name
public|django_content_type|table|user_name
public|django_migrations|table|user_name
public|django_session|table|user_name
public|games_game|table|user_name
public|games_gamecategory|table|user_name
public|games_player|table|user_name
public|games_playerscore|table|user_name
(14rows)
Asseeninourpreviousexample,Djangousesthegames_prefixforthefollowingfourtablenamesrelatedtothegamesapplication.Django'sintegratedORMgeneratedthesetablesandtheforeignkeys,basedontheinformationincludedinourmodels:
games_game:PersiststheGamemodelgames_gamecategory:PersiststheGameCategorymodelgames_player:PersiststhePlayermodelgames_playerscore:PersiststhePlayerScoremodel
ThefollowingcommandwillallowyoutocheckthecontentsofthefourtablesafterwecomposeandsendHTTPrequeststotheRESTfulAPIandmakeCRUDoperationstothefourtables.ThecommandsassumethatyouarerunningPostgreSQLonthesamecomputerinwhichyouarerunningthecommand.
psql--username=user_name--dbname=games--command="SELECT*FROM
games_gamecategory;"
psql--username=user_name--dbname=games--command="SELECT*FROM
games_game;"
psql--username=user_name--dbname=games--command="SELECT*FROM
games_player;"
psql--username=user_name--dbname=games--command="SELECT*FROM
games_playerscore;"
Tip
InsteadofworkingwiththePostgreSQLcommand-lineutility,youcanuseaGUItooltocheckthecontentsofthePostgreSQLdatabase.YoucanalsousethedatabasetoolsincludedinyourfavoriteIDEtocheckthecontentsfortheSQLitedatabase.
Djangogeneratesadditionaltablesthatitrequirestosupportthewebframeworkandtheauthenticationfeaturesthatwewilluselater.
ManagingserializationanddeserializationwithrelationshipsandhyperlinksOurnewRESTfulWebAPIhastobeabletoserializeanddeserializetheGameCategory,Game,Player,andPlayerScoreinstancesintoJSONrepresentations.Inthiscase,wealsohavetopayspecialattentiontotherelationshipsbetweenthedifferentmodelswhenwecreatetheserializerclassestomanageserializationtoJSONanddeserializationfromJSON.
InourlastversionofthepreviousAPI,wecreatedasubclassoftherest_framework.serializers.ModelSerializerclasstomakeiteasiertogenerateaserializerandreduceboilerplatecode.Inthiscase,wewillalsodeclareaclassthatinheritsfromModelSerializer,buttheotherclasseswillinheritfromtherest_framework.serializers.HyperlinkedModelSerializerclass.
TheHyperlinkedModelSerializerisatypeofModelSerializerthatuseshyperlinkedrelationshipsinsteadofprimarykeyrelationships,andtherefore,itrepresentstherealationshipstoothermodelinstanceswithhyperlinksinsteadofprimarykeyvalues.Inaddition,theHyperlinkedModelSerializergeneratedafieldnamedurlwiththeURLfortheresourceasitsvalue.AsseeninthecaseofModelSerializer,theHyperlinkedModelSerializerclassprovidesdefaultimplementationsforthecreateandupdatemethods.
Now,gotothegamesapi/gamesfolderandopentheserializers.pyfile.ReplacethecodeinthisfilewiththefollowingcodethatdeclarestherequiredimportsandtheGameCategorySerializerclass.Wewilladdmoreclassestothisfilelater.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
fromrest_frameworkimportserializers
fromgames.modelsimportGameCategory
fromgames.modelsimportGame
fromgames.modelsimportPlayer
fromgames.modelsimportPlayerScore
importgames.views
classGameCategorySerializer(serializers.HyperlinkedModelSerializer):
games=serializers.HyperlinkedRelatedField(
many=True,
read_only=True,
view_name='game-detail')
classMeta:
model=GameCategory
fields=(
'url',
'pk',
'name',
'games')
TheGameCategorySerializerclassisasubclassoftheHyperlinkedModelSerializerclass.TheGameCategorySerializerclassdeclaresagamesattributeasaninstanceofserializers.HyperlinkedRelatedFieldwithmanyandread_onlyequaltoTruebecauseitisaone-to-manyrelationshipanditisread-only.Weusethegamesnamethatwespecifiedastherelated_namestringvaluewhenwecreatedthegame_categoryfieldasamodels.ForeignKeyinstanceintheGamemodel.Thisway,thegamesfieldwillprovideuswithanarrayofhyperlinkstoeachgamethatbelongtothegamecategory.Theview_namevalueis'game-detail'becausewewantthebrowsableAPIfeaturetousethegamedetailviewtorenderthehyperlinkwhentheuserclicksortapsonit.
TheGameCategorySerializerclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,theGameCategoryclass.Thefieldsattributespecifiesatupleofstringwhosevaluesindicatesthefieldnamesthatwewanttoincludeintheserializationfromtherelatedmodel.WewanttoincludeboththeprimarykeyandtheURL,andtherefore,thecodespecifiedboth'pk'and'url'asmembersofthetuple.Thereisnoneedtooverrideeitherthecreate,orupdatemethodbecausethegenericbehaviorwillbeenoughinthiscase.TheHyperlinkedModelSerializersuperclassprovidesimplementationsforbothmethods.
Now,addthefollowingcodetotheserializers.pyfiletodeclaretheGameSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
classGameSerializer(serializers.HyperlinkedModelSerializer):
#Wewanttodisplaythegamecagory'snameinsteadoftheid
game_category=
serializers.SlugRelatedField(queryset=GameCategory.objects.all(),
slug_field='name')
classMeta:
model=Game
fields=(
'url',
'game_category',
'name',
'release_date',
'played')
TheGameSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.TheGameSerializerclassdeclaresagame_categoryattributeasaninstanceofserializers.SlugRelatedFieldwithitsquerysetargumentsettoGameCategory.objects.all()anditsslug_fieldargumentsetto'name'.ASlugRelatedFieldisaread-writefieldthatrepresentsthetargetoftherelationshipbyauniqueslugattribute,thatis,thedescription.Wecreatedthegame_categoryfieldasamodels.ForeignKeyinstanceintheGamemodelandwewanttodisplaythegamecategory'snameasthedescription(slugfield)fortherelatedGameCategory.Thus,wespecified'name'astheslug_field.IncaseitisnecessarytodisplaythepossibleoptionsfortherelatedgamecategoryinaforminthebrowsableAPI,Djangowillusetheexpressionspecifiedinthequerysetargumenttoretrieveallthepossibleinstancesanddisplaytheirspecifiedslugfield.
TheGameCategorySerializerclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,theGameclass.Thefieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesthatwewanttoincludeintheserializationfromtherelatedmodel.WejustwanttoincludetheURL,andtherefore,thecodespecifiedboth'url'asamemberofthetuple.Thegame_categoryfieldwillspecifythenamefieldfortherelatedGameCategory.
Now,addthefollowingcodetotheserializers.pyfiletodeclaretheScoreSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
classScoreSerializer(serializers.HyperlinkedModelSerializer):
#Wewanttodisplayallthedetailsforthegame
game=GameSerializer()
#Wedon'tincludetheplayerbecauseitwillbenestedintheplayer
classMeta:
model=PlayerScore
fields=(
'url',
'pk',
'score',
'score_date',
'game',
)
TheScoreSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.WewillusetheScoreSerializerclasstoserializePlayerScoreinstancesrelatedtoaPlayer,thatis,todisplayallthescoresforaspecificplayerwhenweserializeaPlayer.WewanttodisplayallthedetailsfortherelatedGamebutwedon'tincludetherelatedPlayerbecausethePlayerwillusethisScoreSerializerserializer.
TheScoreSerializerclassdeclaresagameattributeasaninstanceofthepreviouslycodedGameSerializerclass.Wecreatedthegamefieldasamodels.ForeignKeyinstanceinthePlayerScoremodelandwewanttoserializethesamedataforthegamethatwecodedintheGameSerializerclass.
TheScoreSerializerclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,thePlayerScoreclass.Aspreviouslyexplain,wedon'tincludethe'player'fieldnameinthefieldstupleofstringtoavoidserializingtheplayeragain.WewilluseaPlayerSerializerasamasterandtheScoreSerializerasthedetail.
Now,addthefollowingcodetotheserializers.pyfiletodeclarethePlayerSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
classPlayerSerializer(serializers.HyperlinkedModelSerializer):
scores=ScoreSerializer(many=True,read_only=True)
gender=serializers.ChoiceField(
choices=Player.GENDER_CHOICES)
gender_description=serializers.CharField(
source='get_gender_display',
read_only=True)
classMeta:
model=Player
fields=(
'url',
'name',
'gender',
'gender_description',
'scores',
)
ThePlayerSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.WewillusethePlayerSerializerclasstoserializePlayerinstancesandwewillusethepreviouslydeclaredScoreSerializerclasstoserializeallthePlayerScoreinstancesrelatedtothePlayer.
ThePlayerSerializerclassdeclaresascoresattributeasaninstanceofthepreviouslycodedScoreSerializerclass.ThemanyargumentissettoTruebecauseitisaone-to-manyrelationship.Weusethescoresnamethatwespecifiedastherelated_namestringvaluewhenwecreatedtheplayerfieldasamodels.ForeignKeyinstanceinthePlayerScoremodel.Thisway,thescoresfieldwillrendereachPlayerScorethatbelongstothePlayerusingthepreviouslydeclaredScoreSerializer.
ThePlayermodeldeclaredgenderasaninstanceofmodels.CharFieldwiththechoicesattributesettothePlayer.GENDER_CHOICESstringtuple.TheScoreSerializerclassdeclaresagenderattributeasaninstanceofserializers.ChoiceFieldwiththechoicesargumentsettothePlayer.GENDER_CHOICESstringtuple.Inaddition,theclassdeclaresagender_descriptionattributewithread_onlysettoTrueandthesourceargumentsetto'get_gender_display'.Thesourcestringisbuiltwithget_followedbythefieldname,gender,and_display.Thisway,theread-onlygender_descriptionattributewillrenderthedescriptionforthegenderchoicesinsteadofthesinglecharstoredvalues.
TheScoreSerializerclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,thePlayerScoreclass.Aspreviouslyexplained,wedon'tincludethe'player'fieldnameinthefieldstupleofstringtoavoidserializingtheplayeragain.WewilluseaPlayerSerializerasamasterandtheScoreSerializerasthedetail.
Finally,addthefollowingcodetotheserializers.pyfiletodeclarethePlayerScoreSerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
classPlayerScoreSerializer(serializers.ModelSerializer):
player=serializers.SlugRelatedField(queryset=Player.objects.all(),
slug_field='name')
#Wewanttodisplaythegame'snameinsteadoftheid
game=serializers.SlugRelatedField(queryset=Game.objects.all(),
slug_field='name')
classMeta:
model=PlayerScore
fields=(
'url',
'pk',
'score',
'score_date',
'player',
'game',
)
ThePlayerScoreSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.WewillusethePlayerScoreSerializerclasstoserializePlayerScoreinstances.Previously,wecreatedtheScoreSerializerclasstoserializePlayerScoreinstancesasthedetailofaplayer.WewillusethenewPlayerScoreSerializerclasswhenwewanttodisplaytherelatedplayer'snameandtherelatedgame'sname.Intheotherserializerclass,wedidn'tincludeanyinformationrelatedtotheplayerandweincludedallthedetailsforthegame.
ThePlayerScoreSerializerclassdeclaresaplayerattributeasaninstanceofserializers.SlugRelatedFieldwithitsquerysetargumentsettoPlayer.objects.all()anditsslug_fieldargumentsetto'name'.Wecreatedtheplayerfieldasamodels.ForeignKeyinstanceinthePlayerScoremodelandwewanttodisplaytheplayer'snameasthedescription(slugfield)fortherelatedPlayer.Thus,wespecified'name'astheslug_field.IncaseitisnecessarytodisplaythepossibleoptionsfortherelatedgamecategoryinaforminthebrowsableAPI,Djangowillusetheexpressionspecifiedinthequerysetargumenttoretrieveallthepossibleplayersanddisplaytheirspecifiedslugfield.
ThePlayerScoreSerializerclassdeclaresagameattributeasaninstanceofserializers.SlugRelatedFieldwithitsquerysetargumentsettoGame.objects.all()anditsslug_fieldargumentsetto'name'.Wecreatedthegamefieldasamodels.ForeignKeyinstanceinthePlayerScoremodelandwewanttodisplaythegame'snameasthedescription(slugfield)fortherelatedGame.
Creatingclass-basedviewsandusinggenericclassesThistime,wewillwriteourAPIviewsbydeclaringclass-basedviews,insteadoffunction-basedviews.Wemightcodeclassesthatinheritfromtherest_framework.views.APIViewclassanddeclaremethodswiththesamenamesthantheHTTPverbswewanttoprocess:get,post,put,patch,delete,andsoon.Thesemethodsreceivearequestargumentashappenedwiththefunctionsthatwecreatedfortheviews.However,thisapproachwouldrequireustowritealotofcode.Instead,wecantakeadvantageofasetofgenericviewsthatwecanuseasourbaseclassesforourclass-basedviewstoreducetherequiredcodetotheminimumandtakeadvantageofthebehaviorthathasbeengeneralizedinDjangoRESTFramework.
Wewillcreatesubclassesofthetwofollowinggenericclassviewsdeclaredinrest_framework.generics:
ListCreateAPIView:Implementsthegetmethodthatretrievesalistingofaquerysetandthepostmethodthatcreatesamodelinstance.RetrieveUpdateDestroyAPIView:Implementstheget,put,patch,anddeletemethodstoretreive,completelyupdate,partiallyupdateordeleteamodelinstance.
ThosetwogenericviewsarecomposedbycombiningreusablebitsofbehaviorinDjangoRESTFrameworkimplementedasmixinclassesdeclaredinrest_framework.mixins.Wecancreateaclassthatusesmultipleinheritanceandcombinethefeaturesprovidedbymanyofthesemixinclasses.ThefollowinglineshowsthedeclarationoftheListCreateAPIViewclassasthecompositionofListModelMixin,CreateModelMixinandrest_framework.generics.GenericAPIView:
classListCreateAPIView(mixins.ListModelMixin,
mixins.CreateModelMixin,
GenericAPIView):
ThefollowinglineshowsthedeclarationoftheRetrieveUpdateDestroyAPIViewclassasthecompositionofRetrieveModelMixin,UpdateModelMixin,DestroyModelMixinandrest_framework.generics.GenericAPIView:
classRetrieveUpdateDestroyAPIView(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
GenericAPIView):
Now,wewillcreateaDjangoclassbasedviewsthatwillusethepreviouslyexplainedgenericclassesandtheserializerclassestoreturnJSONrepresentationsforeachHTTPrequestthatourAPIwillhandle.Wewilljusthavetospecifyaquerysetthatretrievesalltheobjectsinthequerysetattributeandtheserializerclassintheserializer_classattributeforeachsubclassthatwedeclare.Thegenericclasseswilldotherestforus.Inaddition,wewilldeclareanameattributewiththestringnamewewillusetoidentifytheview.
TakingadvantageofgenericclassbasedviewsGotothegamesapi/gamesfolderandopentheviews.pyfile.Replacethecodeinthisfilewiththefollowingcodethatdeclarestherequiredimportsandtheclassbasedviews.Wewilladdmoreclassestothisfilelater.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
fromgames.modelsimportGameCategory
fromgames.modelsimportGame
fromgames.modelsimportPlayer
fromgames.modelsimportPlayerScore
fromgames.serializersimportGameCategorySerializer
fromgames.serializersimportGameSerializer
fromgames.serializersimportPlayerSerializer
fromgames.serializersimportPlayerScoreSerializer
fromrest_frameworkimportgenerics
fromrest_framework.responseimportResponse
fromrest_framework.reverseimportreverse
classGameCategoryList(generics.ListCreateAPIView):
queryset=GameCategory.objects.all()
serializer_class=GameCategorySerializer
name='gamecategory-list'
classGameCategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=GameCategory.objects.all()
serializer_class=GameCategorySerializer
name='gamecategory-detail'
classGameList(generics.ListCreateAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-list'
classGameDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-detail'
classPlayerList(generics.ListCreateAPIView):
queryset=Player.objects.all()
serializer_class=PlayerSerializer
name='player-list'
classPlayerDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=Player.objects.all()
serializer_class=PlayerSerializer
name='player-detail'
classPlayerScoreList(generics.ListCreateAPIView):
queryset=PlayerScore.objects.all()
serializer_class=PlayerScoreSerializer
name='playerscore-list'
classPlayerScoreDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=PlayerScore.objects.all()
serializer_class=PlayerScoreSerializer
name='playerscore-detail'
Thefollowingtablesummarizesthemethodsthateachclass-basedviewisgoingtoprocess:
Scope Classbasedviewname
HTTPverbsthatitwillprocess
Collectionofgamecategories-/game-categories/
GameCategoryList GETandPOST
Gamecategory-/game-category/{id}/ GameCategoryDetail GET,PUT,PATCHandDELETE
Collectionofgames-/games/ GameList GETandPOST
Game-/game/{id}/ GameDetail GET,PUT,PATCHandDELETE
Collectionofplayers-/players/ PlayerList GETandPOST
Player-/player/{id}/ PlayerDetail GET,PUT,PATCHandDELETE
Collectionofscores-/player-scores/ PlayerScoreList GETandPOST
Score-/player-score/{id}/ PlayerScoreDetail GET,PUT,PATCHandDELETE
Inaddition,wewillbeabletoexecutetheOPTIONSHTTPverbonanyofthescopes.
WorkingwithendpointsfortheAPIWewanttocreateanendpointfortherootofourAPItomakeiteasiertobrowsetheAPIwiththebrowsableAPIfeatureandunderstandhoweverythingworks.Addthefollowingcodetotheviews.pyfiletodeclaretheApiRootclass.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder.
classApiRoot(generics.GenericAPIView):
name='api-root'
defget(self,request,*args,**kwargs):
returnResponse({
'players':reverse(PlayerList.name,request=request),
'game-categories':reverse(GameCategoryList.name,request=request),
'games':reverse(GameList.name,request=request),
'scores':reverse(PlayerScoreList.name,request=request)
})
TheApiRootclassisasubclassoftherest_framework.generics.GenericAPIViewclassanddeclaresthegetmethod.TheGenericAPIViewclassisthebaseclassforalltheothergenericviews.TheApiRootclassdefinesthegetmethodthatreturnsaResponseobjectwithkey-valuepairsofstringthatprovideadescriptivenamefortheviewanditsURL,generatedwiththerest_framework.reverse.reversefunction.ThisURLresolverfunctionreturnsafullyqualifiedURLfortheview.
Gotothegamesapi/gamesfolderandopentheurls.pyfile.Replacethecodeinthisfilewiththefollowingcode.ThefollowinglinesshowthecodeforthisfilethatdefinestheURLpatternsthatspecifiestheregularexpressionsthathavetobematchedintherequesttorunaspecificmethodforaclass-basedviewdefinedintheviews.pyfile.Insteadofspecifyingafunctionthatrepresentsaviewwecalltheas_viewmethodfortheclass-basedview.Weusetheas_viewmethod.Thecodefileforthesampleisincludedintherestful_python_chapter_02_03folder:
fromdjango.conf.urlsimporturl
fromgamesimportviews
urlpatterns=[
url(r'^game-categories/$',
views.GameCategoryList.as_view(),
name=views.GameCategoryList.name),
url(r'^game-categories/(?P<pk>[0-9]+)/$',
views.GameCategoryDetail.as_view(),
name=views.GameCategoryDetail.name),
url(r'^games/$',
views.GameList.as_view(),
name=views.GameList.name),
url(r'^games/(?P<pk>[0-9]+)/$',
views.GameDetail.as_view(),
name=views.GameDetail.name),
url(r'^players/$',
views.PlayerList.as_view(),
name=views.PlayerList.name),
url(r'^players/(?P<pk>[0-9]+)/$',
views.PlayerDetail.as_view(),
name=views.PlayerDetail.name),
url(r'^player-scores/$',
views.PlayerScoreList.as_view(),
name=views.PlayerScoreList.name),
url(r'^player-scores/(?P<pk>[0-9]+)/$',
views.PlayerScoreDetail.as_view(),
name=views.PlayerScoreDetail.name),
url(r'^$',
views.ApiRoot.as_view(),
name=views.ApiRoot.name),
]
WhenwecodedourpreviousversionoftheAPI,wereplacedthecodeintheurls.pyfileinthegamesapifolder,specifically,thegamesapi/urls.pyfile.WemadethenecessarychangestodefinetherootURLconfigurationandincludetheURLpatterndeclaredinthepreviouslycodedgames/urls.pyfile.
Now,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequeststoourstillunsecure,yetmuchmorecomplexWebAPI(wewilldefinitelyaddsecuritylater).ExecuteanyofthefollowingtwocommandsbasedonyourneedstoaccesstheAPIinotherdevicesorcomputersconnectedtoyourLAN.RememberthatweanalyzedthedifferencebetweentheminChapter1,DevelopingRESTfulAPIswithDjango:
pythonmanage.pyrunserver
pythonmanage.pyrunserver0.0.0.0:8000
Afterwerunanyofthepreviouscommands,thedevelopmentserverwillstartlisteningatport8000.
Openawebbrowserandenterhttp://localhost:8000/ortheappropriateURLincaseyouareusinganothercomputerordevicetoaccessthebrowsableAPI.ThebrowsableAPIwillcomposeandsendaGETrequestto/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONresponsefromtheexecutionofthegetmethoddefinedintheApiRootclasswithintheviews.pyfile.ThefollowingscreenshotshowstherenderedwebpageafterenteringtheURLinawebbrowserwiththeresourcedescription:ApiRoot.
TheAPIRootprovidesushyperlinkstoseethelistofgamecategories,games,players,andscores.Thisway,itbecomesextremelyeasytoaccessthelistsandperformoperationsonthedifferentresourcesthroughthebrowsableAPI.Inaddition,whenwevisittheotherURLs,thebreadcrumbwillallowustogobacktotheApiRoot.
InthisnewversionoftheAPI,weworkedwiththegenericviewsthatprovidemanyfeaturedunderthehoods,andtherefore,thebrowsableAPIwillprovideusadditionalfeaturescomparedwiththepreviousversion.ClickortapontheURLontheright-handsideofgame-categories.Incaseyouarebrowsinginlocalhost,theURLwillbe
http://localhost:8000/game-categories/.ThebrowsableAPIwillrenderthewebpagefortheGameCategoryList.
Atthebottomoftherenderedwebpage,thebrowsableAPIprovidesussomecontrolstogenerateaPOSTrequestto/game-categories/.Inthiscase,bydefault,thebrowsableAPIdisplaystheHTMLformtabwithanautomaticallygeneratedformthatwecanusetogenerateaPOSTrequestwithouthavingtodealwiththerawdataaswedidinourpreviousversion.TheHTMLformsmakeiteasytogeneraterequeststotestourAPI.ThefollowingscreenshotshowstheHTMLformtocreateanewgamecategory:
Wejustneedtoenterthedesiredname,3DRPG,intheNametextboxandclickortaponPOST tocreateanewgamecategory.ThebrowsableAPIwillcomposeandsendaPOSTrequestto/game-categories/withthepreviouslyspecifieddataandwewillseetheresultsofthecallinthewebbrowser.Thefollowingscreenshotshowsawebbrowserdisplayingthe
HTTPstatuscode201CreatedintheresponseandthepreviouslyexplainedHTMLformwiththePOST buttontoallowustocontinuecomposingandsendingPOSTrequeststo/game-categories/:
Now,clickontheURLdisplayedasavaluefortheurlkeyintheJSONdatadisplayedforthegamecategory,suchashttp://localhost:8000/game-categories/3/.Makesureyoureplace2withtheidorprimarykeyofanexistinggamecategoryinthepreviouslyrenderedGamesList.ThebrowsableAPIwillcomposeandsendaGETrequestto/game-categories/3/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONdataforthegamecategory.ThewebpagewilldisplayaDELETEbuttonbecauseweareworkingwiththeGameCategoryDetailview.
Tip
WecanusethebreadcrumbtogobacktotheApiRootandstartcreatinggamesrelatedtoagamecategory,players,andfinallyscoresrelatedtoagameandaplayer.Wecandoallthis
witheasytouseHTMLformsandthebrowsableAPIfeature.
CreatingandretrievingrelatedresourcesNow,wewillusetheHTTPiecommandoritscurlequivalentstocomposeandsendHTTPrequeststotheAPI.WewilluseJSONfortherequeststhatrequireadditionaldata.RememberthatyoucanperformthesametaskswithyourfavoriteGUI-basedtoolorwiththebrowsableAPI.
First,wewillcomposeandsendanHTTPrequesttocreateanewgamecategory.RememberthatweusedthebrowsableAPItocreateagamecategorynamed'3DRPG'.
httpPOST:8000/game-categories/name='2Dmobilearcade'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"2Dmobile
arcade"}':8000/game-categories/
TheprecedingcommandwillcomposeandsendaPOSTHTTPrequestwiththespecifiedJSONkey-valuepair.Therequestspecifies/game-categories/,andtherefore,itwillmatch'^game-categories/$'andrunthepostmethodfortheviews.GameCategoryListclass-basedview.RememberthatthemethodisdefinedintheListCreateAPIViewsuperclassanditendsupcallingthecreatemethoddefinedinmixins.CreateModelMixin.IfthenewGameCategoryinstancewassuccessfullypersistedinthedatabase,thecalltothemethodwillreturnanHTTP201CreatedstatuscodeandtherecentlypersistedGameCategoryserializedtoJSONintheresponsebody.ThefollowinglineshowsasampleresponsefortheHTTPrequestwiththenewGameCategoryobjectintheJSONresponse.Theresponsedoesn'tincludetheheader.Notethattheresponseincludesboththeprimarykey,pk,andtheurl,url,forthecreatedcategory.Thegamesarrayisemptybecausetherearen'tgamesrelatedtothenewcategoryyet:
{
"games":[],
"name":"2Dmobilearcade",
"pk":4,
"url":"http://localhost:8000/game-categories/4/"
}
Now,wewillcomposeandsendHTTPrequeststocreatetwogamesthatbelongtothefirstcategorywerecentlycreated:3DRPG.Wewillspecifythegame_categoryvaluewiththenameofthedesiredgamecategory.However,thedatabasetablethatpersiststheGamemodelwillsavethevalueoftheprimarykeyoftherelatedGameCategorywhosenamevaluematchestheoneweprovide:
httpPOST:8000/games/name='PvZGardenWarfare4'game_category='3DRPG'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='SupermanvsAquaman'game_category='3DRPG'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"PvZGarden
Warfare4","game_category":"3DRPG","played":"false","release_date":
"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Supermanvs
Aquaman","game_category":"3DRPG","played":"false","release_date":"2016-
06-21T03:02:00.776594Z"}':8000/games/
ThepreviouscommandswillcomposeandsendtwoPOSTHTTPrequestswiththespecifiedJSONkey-valuepairs.Therequestspecifies/games/,andtherefore,itwillmatch'^games/$'andrunthepostmethodfortheviews.GameListclass-basedview.ThefollowinglinesshowsampleresponsesforthetwoHTTPrequestswiththenewGameobjectsintheJSONresponses.Theresponsesdon'tincludetheheaders.Notethattheresponseincludesonlytheurl,url,forthecreatedgamesanddoesn'tincludetheprimarykey.Thevalueforgame_categoryisthenamefortherelatedGameCategory:
{
"game_category":"3DRPG",
"name":"PvZGardenWarfare4",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/2/"
}
{
"game_category":"3DRPG",
"name":"SupermanvsAquaman",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/3/"
}
WecanrunthepreviouslyexplainedcommandstocheckthecontentsofthetablesthatDjangocreatedinthePostgreSQLdatabase.Wewillnoticethatthegame_category_idcolumnforthegames_gametablesavesthevalueoftheprimarykeyoftherelatedrowinthegames_game_categorytable.TheGameSerializerclassusestheSlugRelatedFieldtodisplaythenamevaluefortherelatedGameCategory.Thefollowingscreenshotshowsthecontentsofthegames_game_categoryandthegames_gametableinaPostgreSQLdatabaseafterrunningtheHTTPrequests:
Now,wewillcomposeandsendanHTTPrequesttoretrievethegamecategorythatiscontainstwogames,thatisthegamecategoryresourcewhoseidorprimarykeyisequalto3.Don'tforgettoreplace3withtheprimarykeyvalueofthegamewhosenameisequalto'3DRPG'inyourconfiguration:
http:8000/game-categories/3/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/game-categories/3/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/game-categories/3/.Therequesthasanumberafter/game-categories/,andtherefore,itwillmatch'^game-categories/(?P<pk>[0-9]+)/$'andrunthegetmethodfortheviews.GameCategoryDetailclassbasedview.RememberthatthemethodisdefinedintheRetrieveUpdateDestroyAPIViewsuperclassanditendsupcallingtheretrieve
methoddefinedinmixins.RetrieveModelMixin.ThefollowinglinesshowasampleresponsefortheHTTPrequest,withtheGameCategoryobjectandthehyperlinksoftherelatedgamesintheJSONresponse:
HTTP/1.0200OK
Allow:GET,PUT,PATCH,DELETE,HEAD,OPTIONS
Content-Type:application/json
Date:Tue,21Jun201623:32:04GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"games":[
"http://localhost:8000/games/2/",
"http://localhost:8000/games/3/"
],
"name":"3DRPG",
"pk":3,
"url":"http://localhost:8000/game-categories/3/"
}
TheGameCategorySerializerclassdefinedthegamesattributeasaHyperlinkedRelatedField,andtherefore,theserializerrenderstheURLforeachrelatedGameinstanceinthevalueforthegamesarray.IfweviewtheresultsinawebbrowserthroughthebrowsableAPI,wewillbeabletoclickortaponthehyperlinktoseethedetailsforeachgame.
Now,wewillcomposeandsendaPOSTHTTPrequesttocreateagamerelatedtoagamecategorynamethatdoesn'texist:'Virtualreality':
httpPOST:8000/games/name='CaptainAmericavsThor'game_category='Virtual
reality'played=falserelease_date='2016-06-21T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"'Captain
AmericavsThor","game_category":"Virtualreality","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
Djangowon'tbeabletoretrieveaGameCategoryinstancewhosenameisequaltothespecifiedvalue,andtherefore,wewillreceivea400BadRequeststatuscodeintheresponseheaderandamessagerelatedtothevaluespecifiedinforgame_categoryintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BadRequest
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Tue,21Jun201623:51:19GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"game_category":[
"Objectwithname=Virtualrealitydoesnotexist."
]
}
Now,wewillcomposeandsendHTTPrequeststocreatetwoplayers:
httpPOST:8000/players/name='Brandon'gender='M'
httpPOST:8000/players/name='Kevin'gender='M'
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Brandon",
"gender":"M"}':8000/players/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Kevin",
"gender":"M"}':8000/players/
ThepreviouscommandswillcomposeandsendtwoPOSTHTTPrequestswiththespecifiedJSONkey-valuepairs.Therequestspecifies/players/,andtherefore,itwillmatch'^players/$'andrunthepostmethodfortheviews.PlayerListclassbasedview.ThefollowinglinesshowsampleresponsesforthetwoHTTPrequestswiththenewPlayerobjectsintheJSONresponses.Theresponsesdon'tincludetheheaders.Noticethattheresponseincludesonlytheurl,url,forthecreatedplayersanddoesn'tincludetheprimarykey.Thevalueforgender_descriptionisthechoicedescriptionforthegenderchar.Thescoresarrayisemptybecausetherearen'tscoresrelatedtoeachnewplayeryet:
{
"gender":"M",
"name":"Brandon",
"scores":[],
"url":"http://localhost:8000/players/2/"
}
{
"gender":"M",
"name":"Kevin",
"scores":[],
"url":"http://localhost:8000/players/3/"
}
Now,wewillcomposeandsendHTTPrequeststocreatefourscores:
httpPOST:8000/player-scores/score=35000score_date='2016-06-
21T03:02:00.776594Z'player='Brandon'game='PvZGardenWarfare4'
httpPOST:8000/player-scores/score=85125score_date='2016-06-
22T01:02:00.776594Z'player='Brandon'game='PvZGardenWarfare4'
httpPOST:8000/player-scores/score=123200score_date='2016-06-
22T03:02:00.776594Z'player='Kevin'game='SupermanvsAquaman'
httpPOST:8000/player-scores/score=11200score_date='2016-06-
22T05:02:00.776594Z'player='Kevin'game='PvZGardenWarfare4'
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"score":"35000",
"score_date":"2016-06-21T03:02:00.776594Z","player":"Brandon","game":"PvZ
GardenWarfare4"}':8000/player-scores/
curl-iXPOST-H"Content-Type:application/json"-d'{"score":"85125",
"score_date":"2016-06-22T01:02:00.776594Z","player":"Brandon","game":"PvZ
GardenWarfare4"}':8000/player-scores/
curl-iXPOST-H"Content-Type:application/json"-d'{"score":"123200",
"score_date":"2016-06-22T03:02:00.776594Z","player":"Kevin",
"game":"'SupermanvsAquaman"}':8000/player-scores/
curl-iXPOST-H"Content-Type:application/json"-d'{"score":"11200",
"score_date":"2016-06-22T05:02:00.776594Z","player":"Kevin","game":"PvZ
GardenWarfare4"}':8000/player-scores/
ThepreviouscommandswillcomposeandsendfourPOSTHTTPrequestswiththespecifiedJSONkey-valuepairs.Therequestspecifies/player-scores/,andtherefore,itwillmatch'^player-scores/$'andrunthepostmethodfortheviews.PlayerScoreListclassbasedview.ThefollowinglinesshowsampleresponsesforthefourHTTPrequestswiththenewPlayerobjectsintheJSONresponses.Theresponsesdon'tincludetheheaders.
DjangoRESTFrameworkusesthePlayerScoreSerializerclasstogeneratetheJSONresponse.Thus,thevalueforgameisthenamefortherelatedGameinstanceandthevalueforplayeristhenamefortherelatedPlayerinstance.ThePlayerScoreSerializerclassusedSlugRelatedFieldforbothfields:
{
"game":"PvZGardenWarfare4",
"pk":3,
"player":"Brandon",
"score":35000,
"score_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/player-scores/3/"
}
{
"game":"PvZGardenWarfare4",
"pk":4,
"player":"Brandon",
"score":85125,
"score_date":"2016-06-22T01:02:00.776594Z",
"url":"http://localhost:8000/player-scores/4/"
}
{
"game":"SupermanvsAquaman",
"pk":5,
"player":"Kevin",
"score":123200,
"score_date":"2016-06-22T03:02:00.776594Z",
"url":"http://localhost:8000/player-scores/5/"
}
{
"game":"PvZGardenWarfare4",
"pk":6,
"player":"Kevin",
"score":11200,
"score_date":"2016-06-22T05:02:00.776594Z",
"url":"http://localhost:8000/player-scores/6/"
}
WecanrunthepreviouslyexplainedcommandstocheckthecontentsofthetablesthatDjangocreatedinthePostgreSQLdatabase.Wewillnoticethatthegame_idcolumnforthegames_playerscoretablesavesthevalueoftheprimarykeyoftherelatedrowinthegames_gametable.Inaddition,theplayer_idcolumnforthegames_playerscoretablesavesthevalueoftheprimarykeyoftherelatedrowinthegames_playertable.Thefollowingscreenshotshowsthecontentsforthegames_game_category,games_game,games_playerandgames_playerscoretablesinaPostgreSQLdatabaseafterrunningtheHTTPrequests:
Now,wewillcomposeandsendanHTTPrequesttoretrieveaspecificplayerthatcontainstwoscores,whichistheplayerresourcewhoseidorprimarykeyisequalto3.Don'tforgettoreplace3withtheprimarykeyvalueoftheplayerwhosenameisequalto'Kevin'inyourconfiguration:
http:8000/players/3/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/players/3/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8000/players/3/.Therequesthasanumberafter/players/,andtherefore,itwillmatch'^players/(?P<pk>[0-9]+)/$'andrunthegetmethodfortheviews.PlayerDetailclassbasedview.RememberthatthemethodisdefinedintheRetrieveUpdateDestroyAPIViewsuperclassanditendsupcallingtheretrievemethoddefinedinmixins.RetrieveModelMixin.Thefollowinglinesshowasampleresponseforthe
HTTPrequest,withthePlayerobject,therelatedPlayerScoreobjectsandtheGameobjectrelatedtoeachPlayerScoreobjectintheJSONresponse:
HTTP200OK
Allow:GET,PUT,PATCH,DELETE,HEAD,OPTIONS
Content-Type:application/json
Vary:Accept
{
"url":"http://localhost:8000/players/3/",
"name":"Kevin",
"gender":"M",
"gender_description":"Male",
"scores":[
{
"url":"http://localhost:8000/player-scores/5/",
"pk":5,
"score":123200,
"score_date":"2016-06-22T03:02:00.776594Z",
"game":{
"url":"http://localhost:8000/games/3/",
"game_category":"3DRPG",
"name":"SupermanvsAquaman",
"release_date":"2016-06-21T03:02:00.776594Z",
"played":false
}
},
{
"url":"http://localhost:8000/player-scores/6/",
"pk":6,
"score":11200,
"score_date":"2016-06-22T05:02:00.776594Z",
"game":{
"url":"http://localhost:8000/games/2/",
"game_category":"3DRPG",
"name":"PvZGardenWarfare4",
"release_date":"2016-06-21T03:02:00.776594Z",
"played":false
}
}
]
}
ThePlayerSerializerclassdefinedthescoresattributeasaScoreSerializerwithmanyequaltoTrue,andtherefore,thisserializerrenderseachscorerelatedtotheplayer.TheScoreSerializerclassdefinedthegameattributeasaGameSerializer,andtherefore,thisserializerrenderseachgamerelatedtothescore.IfweviewtheresultsinawebbrowserthroughthebrowsableAPI,wewillbeabletoclickortaponthehyperlinkofeachoftherelatedresources.However,inthiscase,wealsoseealltheirdetailswithouthavingtofollowthehyperlink.
Testyourknowledge1. Underthehoods,the@api_viewdecoratoris:
1. Awrapperthatconvertsafunction-basedviewintoasubclassoftherest_framework.views.APIViewclass.
2. Awrapperthatconvertsafunction-basedviewintoaserializer.3. Awrapperthatconvertsafunction-basedviewintoasubclassofthe
rest_framework.views.api_viewclass.
2. ThebrowsableAPI,afeatureincludedinDjangoRESTFrameworkthat:1. Generateshuman-friendlyJSONoutputforeachresourcewhenevertherequest
specifiesapplication/jsonasthevaluefortheContent-typekeyintherequestheader.
2. Generateshuman-friendlyHTMLoutputforeachresourcewhenevertherequestspecifiestext/htmlasthevaluefortheContent-typekeyintherequestheader.
3. Generateshuman-friendlyHTMLoutputforeachresourcewhenevertherequestspecifiesapplication/jsonasthevaluefortheContent-typekeyintherequestheader.
3. Therest_framework.serializers.ModelSerializerclass:1. Automaticallypopulatesbothasetofdefaultconstraintsandasetofdefaultparsers.2. populatesbothasetofdefaultfieldsbutdoesn'tautomaticallypopulateasetof
defaultvalidators.
Automaticallypopulatesbothasetofdefaultfieldsbutdoesn'tautomaticallypopulateasetofdefaultvalidators.Automaticallypopulatesbothasetofdefaultfieldsandasetofdefaultvalidators.
4. Therest_framework.serializers.ModelSerializerclass:1. Providesdefaultimplementationsforthegetandpatchmethods.2. Providesdefaultimplementationsforthegetandputmethods.3. Providesdefaultimplementationsforthecreateandupdatemethods.
5. TheSerializerandModelSerializerclassesinDjangoRESTFrameworkaresimilartothefollowingtwoclassesinDjangoWebFramework:1. FormandModelFormclasses.2. ViewandModelViewclasses.3. ControllerandModelControllerclasses.
SummaryInthischapter,wetookadvantageofthevariousfeaturesincludedinDjangoRESTFrameworkthatallowedustoeliminateduplicatecodeandbuildourAPIreusinggeneralizedbehaviors.Weusedmodelserializers,wrappers,defaultparsing,andrenderingoptions,classbasedviews,andgenericclasses.
WeusedthebrowsableAPIfeatureandwedesignedaRESTfulAPIthatinteractedwithacomplexPostgreSQLdatabase.Wedeclaredrelationshipswiththemodels,managedserializationanddeserializationwithrelationships,andhyperlinks.Finally,wecreatedandretrievedrelatedresourcesandweunderstoodhowthingsworkunderthehoods.
NowthatwehavebuiltacomplexAPIwithDjangoRESTFramework,wewilluseadditionalabstractionsincludedintheframeworktoimproveourAPI,wewilladdsecurityandauthentication,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter3.ImprovingandAddingAuthenticationtoanAPIWithDjangoInthischapter,wewillimprovetheRESTfulAPIthatwestartedinthepreviouschapterandalsoaddauthenticationrelatedsecuritytoit.Wewill:
AdduniqueconstraintstothemodelsUpdateasinglefieldforaresourcewiththePATCHmethodTakeadvantageofpaginationCustomizepaginationclassesUnderstandauthentication,permissionsandthrottlingAddsecurity-relateddatatothemodelsCreateacustomizedpermissionclassforobject-levelpermissionsPersisttheuserthatmakesarequestConfigurepermissionpoliciesSetadefaultvalueforanewrequiredfieldinmigrationsComposerequestswiththenecessaryauthenticationBrowsetheAPIwithauthenticationcredentials
AddinguniqueconstraintstothemodelsOurAPIhasafewissuesthatweneedtosolve.Rightnow,itispossibletocreatemanygamecategorieswiththesamename.Weshouldn'tbeabletodoso,andtherefore,wewillmakethenecessarychangestotheGameCategorymodeltoaddauniqueconstraintonthenamefield.WewillalsoaddauniqueconstraintonthenamefieldfortheGameandPlayermodels.Thisway,wewilllearnthenecessarystepstomakechangestotheconstraintsformanymodelsandreflectthechangesintheunderlyingdatabasethroughmigrations.
MakesurethatyouquitDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheterminalorCommandPromptwindowinwhichitisrunning.Now,wewillmakechangestointroduceuniqueconstraintstothenamefieldforthemodelsthatweusetorepresentandpersistthegamecategories,games,andplayers.Openthegames/models.py,fileandreplacethecodethatdeclarestheGameCategory,GameandPlayerclasseswiththefollowingcode.Thethreelinesthatchangearehighlightedinthecodelisting.ThecodeforthePlayerScoreclassremainsthesame.Thecodefileforthesampleisincludedintherestful_python_chapter_03_01folder,asshown:
classGameCategory(models.Model):
name=models.CharField(max_length=200,unique=True)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
classGame(models.Model):
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=200,unique=True)
game_category=models.ForeignKey(
GameCategory,
related_name='games',
on_delete=models.CASCADE)
release_date=models.DateTimeField()
played=models.BooleanField(default=False)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
classPlayer(models.Model):
MALE='M'
FEMALE='F'
GENDER_CHOICES=(
(MALE,'Male'),
(FEMALE,'Female'),
)
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=50,blank=False,default='',
unique=True)
gender=models.CharField(
max_length=2,
choices=GENDER_CHOICES,
default=MALE,
)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
Wejustneededtoaddunique=Trueasoneofthenamedargumentsformodels.CharField.Thisway,weindicatethatthefieldmustbeuniqueandDjangowillcreatethenecessaryuniqueconstraintsforthefieldsintheunderlyingdatabasetables.
Now,runthefollowingPythonscripttogeneratethemigrationsthatwillallowustosynchronizethedatabasewiththeuniqueconstraintsweaddedforthefieldsinthemodels:
pythonmanage.pymakemigrationsgames
Thefollowinglinesshowtheoutputgeneratedafterrunningthepreviouscommand:
Migrationsfor'games':
0002_auto_20160623_2131.py:
-Alterfieldnameongame
-Alterfieldnameongamecategory
-Alterfieldnameonplayer
Theoutputindicatesthatthegamesapi/games/migrations/0002_auto_20160623_2131.pyfileincludesthecodetoalterthefieldnamednameongame,gamecategory,andplayer.Notethatthegeneratedfilenamewillbedifferentinyourconfigurationbecauseitincludesanencodeddateandtime.Thefollowinglinesshowthecodeforthisfile,whichwasautomaticallygeneratedbyDjango.Thecodefileforthesampleisincludedintherestful_python_chapter_03_01folder:
#-*-coding:utf-8-*-
#GeneratedbyDjango1.9.7on2016-06-2321:31
from__future__importunicode_literals
fromdjango.dbimportmigrations,models
classMigration(migrations.Migration):
dependencies=[
('games','0001_initial'),
]
operations=[
migrations.AlterField(
model_name='game',
name='name',
field=models.CharField(max_length=200,unique=True),
),
migrations.AlterField(
model_name='gamecategory',
name='name',
field=models.CharField(max_length=200,unique=True),
),
migrations.AlterField(
model_name='player',
name='name',
field=models.CharField(default='',max_length=50,unique=True),
),
]
Thecodedefinesasubclassofthedjango.db.migrations.MigrationclassnamedMigrationthatdefinesanoperationslistwithmanymigrations.AlterField.Eachmigrations.AlterFieldwillalterthefieldinthethetableforeachoftherelatedmodels.
Now,runthefollowingPythonscripttoapplyallthegeneratedmigrationsandexecutethechangesinthedatabasetables:
pythonmanage.pymigrate
Thefollowinglinesshowtheoutputgeneratedafterrunningthepreviouscommand.Notethattheorderingforthemigrationsmightbedifferentinyourconfiguration.
Operationstoperform:
Operationstoperform:
Applyallmigrations:admin,auth,contenttypes,games,sessions
Runningmigrations:
Renderingmodelstates...DONE
Applyinggames.0002_auto_20160623_2131...OK
Afterweruntheprecedingcommand,wewillhaveuniqueindexesonthenamefieldforthegames_game,games_gamecategory,andgames_playertablesinthePostgreSQLdatabase.WecanusethePostgreSQLcommandlineoranyotherapplicationthatallowsustoeasilycheckthecontentsofthePostreSQLdatabasetocheckthetablesthatDjangoupdated.IncaseyoudecidetocontinueworkingwithSQLite,usethecommandsortoolsrelatedtothisdatabase.
Now,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequests.ExecuteanyofthefollowingtwocommandsbasedonyourneedstoaccesstheAPIinotherdevicesorcomputersconnectedtoyourLAN.RememberthatweanalyzedthedifferencebetweentheminChapter1,DevelopingRESTfulAPIswithDjango:
pythonmanage.pyrunserver
pythonmanage.pyrunserver0.0.0.0:8000
Afterwerunanyofthepreviouscommands,thedevelopmentserverwillstartlisteningatport8000.
Now,wewillcomposeandsendanHTTPrequesttocreateagamecategorywithanamethatalreadyexists:'3DRPG':
httpPOST:8000/game-categories/name='3DRPG'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"3DRPG"}'
:8000/game-categories/
Djangowon'tbeabletopersistaGameCategoryinstancewhosenameisequaltothespecifiedvaluebecauseitwouldviolatetheuniqueconstraintaddedtothenamefield.Thus,wewillreceivea400BadRequeststatuscodeintheresponseheaderandamessagerelatedtothevaluespecifiedfornameintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BadRequest
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Sun,26Jun201603:37:05GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"name":[
"GameCategorywiththisnamealreadyexists."
]
}
Afterwehavemadethechanges,wewon'tbeabletoaddduplicatevaluesforthenamefieldingamecategories,games,orplayers.Thisway,wecanbesurethatwheneverwespecifythenameofanyoftheseresources,wearegoingtoreferencethesameuniqueresource.
UpdatingasinglefieldforaresourcewiththePATCHmethodAsweexplainedinChapter2,WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjango,ourAPIcanupdateasinglefieldforanexistingresource,andtherefore,weprovideanimplementationforthePATCHmethod.Forexample,wecanusethePATCHmethodtoupdateanexistinggameandsetthevalueforitsplayedfieldtotrue.Wedon'twanttousethePUTmethodbecausethismethodismeanttoreplaceanentiregame.ThePATCHmethodismeanttoapplyadeltatoanexistinggame,andtherefore,itistheappropriatemethodtojustchangethevalueoftheplayedfield.
Now,wewillcomposeandsendanHTTPrequesttoupdateanexistinggame,specifically,toupdatethevalueoftheplayedfieldandsetittotruebecausewejustwanttoupdateasinglefield,wewillusethePATCHmethodinsteadofPUT.Makesureyoureplace2withtheidorprimarykeyofanexistinggameinyourconfiguration:
httpPATCH:8000/games/2/played=true
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d'{"played":"true"}'
:8000/games/2/
TheprecedingcommandwillcomposeandsendaPATCHHTTPrequestwiththespecifiedJSONkey-valuepair.Therequesthasanumberafter/games/,andtherefore,itwillmatch'^games/(?P<pk>[0-9]+)/$'andrunthepatchmethodfortheviews.GameDetailclass-basedview.RememberthatthemethodisdefinedintheRetrieveUpdateDestroyAPIViewsuperclassanditendsupcallingtheupdatemethoddefinedinmixins.UpdateModelMixin.IftheGameinstanceswiththeupdatedvaluefortheplayedfieldarevalidandweresuccessfullypersistedinthedatabase,thecalltothemethodwillreturna200OKstatuscodeandtherecentlyupdatedGameserializedtoJSONintheresponsebody.Thefollowinglinesshowasampleresponse:
HTTP/1.0200OK
Allow:GET,PUT,PATCH,DELETE,HEAD,OPTIONS
Content-Type:application/json
Date:Sun,26Jun201604:09:22GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"game_category":"3DRPG",
"name":"PvZGardenWarfare4",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/2/"
}
TakingadvantageofpaginationOurdatabasehasafewrowsineachofthetablesthatpersistthemodelswehavedefined.However,afterwestartworkingwithourAPIinareal-lifeproductionenvironment,wewillhavethousandsofplayerscores,players,games,andgamecategories,andtherefore,wewillhavetodealwithlargeresultsets.WecantakeadvantageofthepaginationfeaturesavailableinDjangoRESTFrameworktomakeiteasytospecifyhowwewantlargeresultssetstobesplitintoindividualpagesofdata.
First,wewillcomposeandsendHTTPrequeststocreate10gamesthatbelongtooneofthecategorieswehavecreated:2Dmobilearcade.Thisway,wewillhaveatotalof12gamesthatpersistinthedatabase.Wehad2gamesandwewilladd10more:
httpPOST:8000/games/name='TetrisReloaded'game_category='2Dmobile
arcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='PuzzleCraft'game_category='2Dmobilearcade'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='Blek'game_category='2Dmobilearcade'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='ScribblenautsUnlimited'game_category='2D
mobilearcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='CuttheRope:Magic'game_category='2Dmobile
arcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='TinyDiceDungeon'game_category='2Dmobile
arcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='ADarkRoom'game_category='2Dmobilearcade'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='Bastion'game_category='2Dmobilearcade'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='WelcometotheDungeon'game_category='2Dmobile
arcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
httpPOST:8000/games/name='Dust:AnElysianTail'game_category='2Dmobile
arcade'played=falserelease_date='2016-06-21T03:02:00.776594Z'
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Tetris
Reloaded","game_category":"2Dmobilearcade","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"PuzzleCraft",
"game_category":"2Dmobilearcade","played":"false","release_date":"2016-
06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Blek",
"game_category":"2Dmobilearcade","played":"false","release_date":"2016-
06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Scribblenauts
Unlimited","game_category":"2Dmobilearcade","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"CuttheRope:
Magic","game_category":"2Dmobilearcade","played":"false","release_date":
"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"TinyDice
Dungeon","game_category":"2Dmobilearcade","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"ADarkRoom",
"game_category":"2Dmobilearcade","played":"false","release_date":"2016-
06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Bastion",
"game_category":"2Dmobilearcade","played":"false","release_date":"2016-
06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Welcometothe
Dungeon","game_category":"2Dmobilearcade","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Dust:An
ElysianTail","game_category":"2Dmobilearcade","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
TheprecedingcommandswillcomposeandsendtenPOSTHTTPrequestswiththespecifiedJSONkey-valuepairs.Therequestspecifies/games/,andtherefore,itwillmatch'^games/$'andrunthepostmethodfortheviews.GameListclass-basedview.
Now,wehave12gamesinourdatabase.However,wedon'twanttoretrievethe12gameswhenwecomposeandsendaGETHTTPrequestto/games/.WewillconfigureoneofthecustomizablepaginationstylesincludedinDjangoRESTFrameworktoincludeamaximumoffiveresourcesineachindividualpageofdata.
Tip
OurAPIusesthegenericviewsthatworkwiththemixinclassesthatcanhandlepaginatedresponses,andtherefore,theywillautomaticallytakeintoaccountthepaginationsettingsweconfigureinDjangoRESTFramework.
Openthegamesapi/settings.pyfileandaddthefollowinglinesthatdeclareadictionarynamedREST_FRAMEWORKwithkey-valuepairsthatconfiguretheglobalpaginationsettings.Thecodefileforthesampleisincludedintherestful_python_chapter_03_02folder:
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS':
'rest_framework.pagination.LimitOffsetPagination',
'PAGE_SIZE':5
}
ThevaluefortheDEFAULT_PAGINATION_CLASSsettingskeyspecifiesaglobalsettingwiththedefaultpaginationclassthatthegenericviewswillusetoprovidepaginatedresponses.Inthiscase,wewillusetherest_framework.pagination.LimitOffsetPaginationclass,thatprovidesalimit/offset-basedstyle.Thispaginationstyleworkswithlimitthatindicatesthemaximumnumberofitemstoreturnandanoffsetthatspecifiesthestartingpositionofthequery.ThevalueforthePAGE_SIZEsettingskeyspecifiesaglobalsettingwiththedefaultvalueforthelimit,alsoknownaspagesize.WecanspecifyadifferentlimitwhenweperformtheHTTPrequestbyspecifyingthedesiredvalueinthelimitqueryparameter.Wecanconfiguretheclasstohavethemaximumlimitvalueinordertoavoidtheundesiredhugeresultsets.
Now,wewillcomposeandsendanHTTPrequesttoretrieveallthegames,specificallythefollowingHTTPGETmethodto/games/:
httpGET:8000/games/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/games/
Thegenericviewswillusethenewsettingsthatweaddedtoenabletheoffset/limitpaginationandtheresultwillprovideusthefirst5gameresources(resultskey),thetotalnumberofgamesforthequery(countkey),andalinktothenext(nextkey)andprevious(previouskey)pages.Inthiscase,theresultsetisthefirstpage,andtherefore,thelinktothepreviouspage(previouskey)isnull.Wewillreceivea200OKstatuscodeintheresponseheaderandthe5gamesintheresultsarray:
HTTP/1.0200OK
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Fri,01Jul201600:57:55GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"count":12,
"next":"http://localhost:8000/games/?limit=5&offset=5",
"previous":null,
"results":[
{
"game_category":"2Dmobilearcade",
"name":"ADarkRoom",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/10/"
},
{
"game_category":"2Dmobilearcade",
"name":"Bastion",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/11/"
},
{
"game_category":"2Dmobilearcade",
"name":"Blek",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/6/"
},
{
"game_category":"2Dmobilearcade",
"name":"CuttheRope:Magic",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/8/"
},
{
"game_category":"2Dmobilearcade",
"name":"Dust:AnElysianTail",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/13/"
}
]
}
IntheprecedingHTTPrequest,wedidn'tspecifyanyvalueforeitherthelimitoroffsetparameters.However,aswespecifiedthedefaultvalueoflimitas5itemsintheglobalsettings,thegenericviewsusethisconfigurationvalueandprovideuswiththefirstpage.IfwecomposeandsendthefollowingHTTPrequesttoretrievethefirstpageofallthegamesbyspecifying1fortheoffsetvalue,theAPIwillprovidethesameresultsshownbefore:
httpGET':8000/games/?offset=0'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?offset=0'
IfwecomposeandsendthefollowingHTTPrequesttoretrievethefirstpageofallthegamesbyspecifying0fortheoffsetvalueand5forthelimit,theAPIwillalsoprovidethesameresultsasshownearlier:
httpGET':8000/games/?limit=5&offset=0'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?limit=5&offset=0'
Now,wewillcomposeandsendanHTTPrequesttoretrievethenextpage,thatis,thesecondpageforthegames,specificallyanHTTPGETmethodto/games/withtheoffsetvaluesetto5.RememberthatthevalueforthenextkeyreturnedintheJSONbodyofthepreviousresultprovidesuswiththeURLtothenextpage:
httpGET':8000/games/?limit=5&offset=5'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?limit=5&offset=5'
Theresultwillprovideusthesecondsetofthe5gameresource(resultskey),thetotalnumberofgamesforthequery(countkey),andalinktothenext(nextkey)andprevious(previouskey)pages.Inthiscase,theresultsetisthesecondpage,andtherefore,thelinktothepreviouspage(previouskey)ishttp://localhost:8000/games/?limit=5.Wewillreceivea200OKstatuscodeintheresponseheaderandthe5gamesintheresultsarray:
HTTP/1.0200OK
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Fri,01Jul201601:25:10GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"count":12,
"next":"http://localhost:8000/games/?limit=5&offset=10",
"previous":"http://localhost:8000/games/?limit=5",
"results":[
{
"game_category":"2Dmobilearcade",
"name":"PuzzleCraft",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/5/"
},
{
"game_category":"3DRPG",
"name":"PvZGardenWarfare4",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/2/"
},
{
"game_category":"2Dmobilearcade",
"name":"ScribblenautsUnlimited",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/7/"
},
{
"game_category":"3DRPG",
"name":"SupermanvsAquaman",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/3/"
},
{
"game_category":"2Dmobilearcade",
"name":"TetrisReloaded",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/4/"
}
]
}
IntheprecedingHTTPrequest,wespecifiedvaluesforboththelimitandoffsetparameters.However,aswespecifiedthedefaultvalueoflimitin5itemsintheglobalsettings,thefollowingrequestwillproducethesameresultsthanthepreviousrequest:
httpGET':8000/games/?offset=5'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?offset=5'
Finally,wewillcomposeandsendanHTTPrequesttoretrievethelastpage,thatis,thethirdpageforthegames,specificallyanHTTPGETmethodto/games/withtheoffsetvaluesetto10.RememberthatthevalueforthenextkeyreturnedintheJSONbodyofthepreviousresultprovidesuswiththeURLtothenextpage:
httpGET':8000/games/?limit=5&offset=10'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?limit=5&offset=10'
Theresultwillprovideusthelastsetwith2gameresources(resultskey),thetotalnumberofgamesforthequery(countkey),andalinktothenext(nextkey)andprevious(previouskey)pages.Inthiscase,theresultsetisthelastpage,andtherefore,thelinktothenextpage(nextkey)isnull.Wewillreceivea200OKstatuscodeintheresponseheaderandthe2gamesintheresultsarray:
HTTP/1.0200OK
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Fri,01Jul201601:28:13GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"count":12,
"next":null,
"previous":"http://localhost:8000/games/?limit=5&offset=5",
"results":[
{
"game_category":"2Dmobilearcade",
"name":"TinyDiceDungeon",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/9/"
},
{
"game_category":"2Dmobilearcade",
"name":"WelcometotheDungeon",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/12/"
}
]
}
CustomizingpaginationclassesTherest_framework.pagination.LimitOffsetPaginationclassthatweareusingtoprovidepaginatedresponsesdeclaresamax_limitclassattributethatdefaultstoNone.Thisattributeallowsustoindicatethemaximumallowablelimitthatcanbespecifiedusingthelimitqueryparameter.Withthedefaultsetting,thereisnolimitandwewillbeabletoprocessrequeststhatspecifyavaluefor1000000forthelimitqueryparameter.Wedefinitelydon'twantourAPItobeabletogeneratearesponsewithamillionplayerscoresorplayerswithasinglerequest.Unluckily,thereisnosettingthatallowsustochangethevaluethattheclassassignstothemax_limitclassattribute.Thus,wewillcreateourcustomizedversionofthelimit/offsetpaginationstyleprovidedbyDjangoRESTFramework.
CreateanewPythonfilenamedpagination.pywithinthegamesfolderandenterthefollowingcodewhichdeclaresthenewLimitOffsetPaginationWithMaxLimitclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_03folder:
fromrest_framework.paginationimportLimitOffsetPagination
classLimitOffsetPaginationWithMaxLimit(LimitOffsetPagination):
max_limit=10
TheprecedinglinesdeclaretheLimitOffsetPaginationWithMaxLimitclassasasubclassoftherest_framework.pagination.LimitOffsetPaginationclassandoverridesthevaluespecifiedforthemax_limitclassattributewith10.
Openthegamesapi/settings.pyfileandreplacethelinethatspecifiedthevaluefortheDEFAULT_PAGINATION_CLASSkeyinthedictionarynamedREST_FRAMEWORKwiththehighlightedline.ThefollowinglinesshowthenewdeclarationofthedictionarynamedREST_FRAMEWORK.Thecodefileforthesampleisincludedintherestful_python_chapter_03_03folder:
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS':
'games.pagination.LimitOffsetPaginationWithMaxLimit',
'PAGE_SIZE':5
}
Now,thegenericviewswillusetherecentlydeclaredgames.pagination.LimitOffsetPaginationWithMaxLimitclass,thatprovidesalimit/offsetbasedstylewithamaximumlimitvalueequalto10.Ifarequestspecifiesavalueforlimithigherthan10,theclasswillusethemaximumlimitvalue,thatis,10,andwewillneverreturnmorethan10itemsinapaginatedresponse.
Now,wewillcomposeandsendanHTTPrequesttoretrievethefirstpageforthegames,specificallyanHTTPGETmethodto/games/withthelimitvaluesetto10000:
httpGET':8000/games/?limit=10000'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?limit=10000'
Theresultwillusealimitvalueequalto10insteadoftheindicated10000becauseweareusingourcustomizedpaginationclass.Theresultwillprovideusthefirstsetwith10gameresources(resultskey),thetotalnumberofgamesforthequery(countkey),andalinktothenext(nextkey)andprevious(previouskey)pages.Inthiscase,theresultsetisthefirstpage,andtherefore,thelinktothenextpage(nextkey)ishttp://localhost:8000/games/?limit=10&offset=10.Wewillreceivea200OKstatuscodeintheresponseheaderandthefirst10gamesintheresultsarray.Thefollowinglinesshowtheheaderandthefirstlinesoftheoutput:
HTTP/1.0200OK
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Fri,01Jul201616:34:01GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"count":12,
"next":"http://localhost:8000/games/?limit=10&offset=10",
"previous":null,
"results":[
{
Tip
Itisagoodpracticetoconfigureamaximumlimittoavoidgeneratinghugeresponses.
Openawebbrowserandenterhttp://localhost:8000/games/.ReplacelocalhostwiththeIPofthecomputerthatisrunningtheDjangodevelopmentserverincaseyouuseanothercomputerordevicetorunthebrowser.ThebrowsableAPIwillcomposeandsendaGETrequestto/games/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONgameslist;sincewehaveconfiguredpagination,therenderedwebpagewillincludethedefaultpaginationtemplateassociatedwiththebasepaginationclassweareusingandwilldisplaytheavailablepagenumbersattheupper-rightcornerofthewebpage.ThefollowingscreenshotshowstherenderedwebpageafterenteringtheURLinawebbrowserwiththeresourcedescription,GameList,andthethreepages.
Understandingauthentication,permissionsandthrottlingOurcurrentversionoftheAPIprocessesalltheincomingrequestswithoutrequiringanykindofauthentication.DjangoRESTFrameworkallowsustoeasilyusedifferentauthenticationschemestoidentifytheuserthatoriginatedtherequestorthetokenthatsignedtherequest.Then,wecanusethesecredentialstoapplythepermissionandthrottlingpoliciesthatwilldeterminewhethertherequestmustbepermittedornot.
Similartootherconfigurations,wecansettheauthenticationschemesgloballyandthenoverridethemifnecessaryinaclass-basedvieworafunctionview.Alistofclassesspecifiestheauthenticationschemes.DjangoRESTframeworkwilluseallthespecifiedclassesinthelisttoauthenticatearequestbeforerunningthecodefortheview.Thefirstclassinthelistthatgeneratesasuccessfulauthentication,incasewespecifymorethanoneclass,willberesponsibleforsettingthevaluesforthefollowingtwoproperties:
request.user:Theusermodelinstance.Wewilluseaninstanceofthedjango.contrib.auth.Userclass,thatis,aDjangoUserinstance,inourexamples.request.auth:Additionalauthenticationinformation,suchasanauthenticationtoken.
Afterasuccessfulauthentication,wecanusetherequest.userpropertyinourclass-basedviewmethodsthatreceivetherequestparametertoretrieveadditionalinformationabouttheuserthatgeneratedtherequest.
DjangoRESTFrameworkprovidesthefollowingthreeauthenticationclassesintherest_framework.authenticationmodule.AllofthemaresubclassesoftheBaseAuthenticationclass:
BasicAuthentication:ProvidesanHTTPBasicauthenticationagainstusernameandpassword.Ifweuseinproduction,wemustmakesurethattheAPIisonlyavailableoverHTTPS.SessionAuthentication:WorkswithDjango'ssessionframeworkforauthentication.TokenAuthentication:Providesasimpletokenbasedauthentication.TherequestmustincludethetokengeneratedforauserintheAuthorizationHTTPheaderwith"Token"asaprefixforthetoken.
First,wewilluseacombinationofBasicAuthenticationandSessionAuthentication.WecouldalsotakeadvantageoftheTokenAuthenticationclasslater.MakesureyouquittheDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+Cintheterminalorcommand-promptwindowinwhichitisrunning.
Openthegamesapi/settings.pyfileandaddthehighlightedlinestothedictionarynamedREST_FRAMEWORKwithakey-valuepairthatconfigurestheglobaldefaultauthenticationclasses.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04
folder,asshown:
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS':
'games.pagination.LimitOffsetPaginationWithMaxLimit',
'PAGE_SIZE':5,
'DEFAULT_AUTHENTICATION_CLASSES':(
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
)
}
ThevaluefortheDEFAULT_AUTHENTICATION_CLASSESsettingskeyspecifiesaglobalsettingwithatupleofstringwhosevaluesindicatetheclassesthatwewanttouseforauthentication.
Permissionsusetheauthenticationinformationincludedintherequest.userandrequest.authpropertiestodeterminewhethertherequestshouldbegrantedordeniedaccess.PermissionsallowustocontrolwhichclassesofuserswillbegrantedordeniedaccesstothedifferentfeaturesorpartsofourAPI.
Forexample,wewillusethepermissionsfeaturesinDjangoRESTframeworktoallowtheauthenticateduserstocreategames.Unauthenticateduserswillonlybeallowedread-onlyaccesstogames.Onlytheuserthatcreatedthegamewillbeabletomakechangestothisgame,andtherefore,wewillmakethenecessarychangesinourAPItomakeagamehaveanowneruser.Wewillusepredefinedpermissionclassesandacustomizedpermissionclasstodefinetheexplainedpermissionpolicies.
Throttlingalsodetermineswhethertherequestmustbeauthorized.ThrottlescontroltherateofrequeststhatuserscanmaketoourAPI.Forexample,wewanttolimitunauthenticateduserstoamaximumof5requestsperhour.Wewanttorestrictauthenticateduserstoamaximumof20requeststothegamesrelatedviewsperday.
Addingsecurity-relateddatatothemodelsWewillassociateagamewithacreatororowner.Onlytheauthenticateduserswillbeabletocreatenewgames.Onlythecreatorofagamewillbeabletoupdateitordeleteit.Alltherequeststhataren'tauthenticatedwillonlyhaveread-onlyaccesstogames.
Openthegames/models.pyfileandreplacethecodethatdeclarestheGameclasswiththefollowingcode.Thelinethatchangesishighlightedinthecodelisting.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder.
classGame(models.Model):
owner=models.ForeignKey(
'auth.User',
related_name='games',
on_delete=models.CASCADE)
created=models.DateTimeField(auto_now_add=True)
name=models.CharField(max_length=200,unique=True)
game_category=models.ForeignKey(
GameCategory,
related_name='games',
on_delete=models.CASCADE)
release_date=models.DateTimeField()
played=models.BooleanField(default=False)
classMeta:
ordering=('name',)
def__str__(self):
returnself.name
TheGamemodeldeclaresanewownerfieldthatusesthedjango.db.models.ForeignKeyclasstoprovideamany-to-onerelationshiptotheauth.Usermodel,specifically,tothedjango.contrib.auth.Usermodel.ThisUsermodelrepresentstheuserswithintheDjangoauthenticationsystem.The'games'valuespecifiedfortherelated_nameargumentcreatesabackwardsrelationfromtheUsermodeltotheGamemodel.ThisvalueindicatesthenametobeusedfortherelationfromtherelatedUserobjectbacktoaGameobject.Thisway,wewillbeabletoaccessallthegamesownedbyaspecificuser.Wheneverwedeleteauser,wewantallthegamesownedbythisusertobedeletedtoo,andtherefore,wespecifiedthemodels.CASCADEvaluefortheon_deleteargument.
Now,wewillrunthecreatesuperusersubcommandformanage.pytocreatethesuperuserforDjangothatwewillusetoeasilyauthenticateourrequests.Wewillcreatemoreuserslater:
pythonmanage.pycreatesuperuser
Thecommandwillaskyoufortheusernameyouwanttouseforthesuperuser.EnterthedesiredusernameandpressEnter.Wewillusesuperuserastheusernameforthisexample.Youwillseealinesimilartothefollowingone:
Username(leaveblanktouse'gaston'):
Then,thecommandwillaskyouforthee-mailaddress.Enterane-mailaddressandpressEnter:
Emailaddress:
Finally,thecommandwillaskyouforthepasswordforthenewsuperuser.EnteryourdesiredpasswordandpressEnter.
Password:
Thecommandwillaskyoutoenterthepasswordagain.EnteritandpressEnter.Ifbothenteredpasswordsmatch,thesuperuserwillbecreated:
Password(again):
Superusercreatedsuccessfully.
Now,gotothegamesapi/gamesfolderandopentheserializers.pyfile.Addthefollowingcodeafterthelastlinethatdeclarestheimports,beforethedeclarationoftheGameCategorySerializerclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
fromdjango.contrib.auth.modelsimportUser
classUserGameSerializer(serializers.HyperlinkedModelSerializer):
classMeta:
model=Game
fields=(
'url',
'name')
classUserSerializer(serializers.HyperlinkedModelSerializer):
games=UserGameSerializer(many=True,read_only=True)
classMeta:
model=User
fields=(
'url',
'pk',
'username',
'games')
TheUserGameSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.Weusethisnewserializerclasstoserializethegamesrelatedtoauser.ThisclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,theGameclass.Thefieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesthatwewanttoincludeintheserializationfromtherelatedmodel.WejustwanttoincludetheURLandthegame'sname,andtherefore,thecodespecified'url'and'name'asmembersofthetuple.Wedon'twanttousethe
GameSerializerserializerclassforthegamesrelatedtoauserbecausewewanttoserializefewerfields,andtherefore,wecreatedtheUserGameSerializerclass.
TheUserSerializerclassisasubclassoftheHyperlinkedModelSerializerclass.ThisclassdeclaresaMetainnerclassthatdeclarestwoattributes-modelandfields.Themodelattributespecifiesthemodelrelatedtotheserializer,thatis,thedjango.contrib.auth.models.Userclass.
TheUserSerializerclassdeclaresagamesattributeasaninstanceofthepreviouslyexplainedUserGameSerializerwithmanyandread_onlyequaltoTruebecauseitisaone-to-manyrelationshipanditisread-only.Weusethegamesnamethatwespecifiedastherelated_namestringvaluewhenweaddedtheownerfieldasamodels.ForeignKeyinstanceintheGamemodel.Thisway,thegamesfieldwillprovideuswithanarrayofURLsandnamesforeachgamethatbelongstotheuser.
Wewillmakemorechangestotheserializers.pyfileinthegamesapi/gamesfolder.WewilladdanownerfieldtotheexistingGameSerializerclass.ThefollowinglinesshowthenewcodefortheGameSerializerclass.Thenewlinesarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
classGameSerializer(serializers.HyperlinkedModelSerializer):
#Wejustwanttodisplaytheownerusername(read-only)
owner=serializers.ReadOnlyField(source='owner.username')
#Wewanttodisplaythegamecagory'snameinsteadoftheid
game_category=
serializers.SlugRelatedField(queryset=GameCategory.objects.all(),
slug_field='name')
classMeta:
model=Game
depth=4
fields=(
'url',
'owner',
'game_category',
'name',
'release_date',
'played')
Now,theGameSerializerclassdeclaresanownerattributeasaninstanceofserializers.ReadOnlyFieldwithsourceequalto'owner.username'.Thisway,wewillserializethevaluefortheusernamefieldoftherelateddjango.contrib.auth.Userholdintheownerfield.WeusetheReadOnlyFieldbecausetheownerisautomaticallypopulatedwhenanauthenticatedusercreatesagame,andtherefore,itwon'tbepossibletochangetheownerafteragamehasbeencreated.Thisway,theownerfieldwillprovideuswiththeusernamethatcreatedthegame.Inaddition,weadded'owner'tothefield'sstringtuple.
Creatingacustomizedpermissionclassforobject-levelpermissionsCreateanewPythonfilenamedpermissions.pywithinthegamesfolderandenterthefollowingcodethat,declaresthenewIsOwnerOrReadOnlyclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
fromrest_frameworkimportpermissions
classIsOwnerOrReadOnly(permissions.BasePermission):
defhas_object_permission(self,request,view,obj):
ifrequest.methodinpermissions.SAFE_METHODS:
returnTrue
else:
returnobj.owner==request.user
Therest_framework.permissions.BasePermissionclassisthebaseclassfromwhichallpermissionclassesshouldinherit.ThepreviouslinesdeclaretheIsOwnerOrReadOnlyclassasasubclassoftheBasePermissionclassandoverridesthehas_object_permissionmethoddefinedinthesuperclassthatreturnsaboolvalueindicatingwhetherthepermissionshouldbegrantedornot.IftheHTTPverbspecifiedintherequest(request.method)isanyofthethreesafemethodsspecifiedinpermission.SAFE_METHODS(GET,HEAD,orOPTIONS),thehas_object_permissionmethodreturnsTrueandgrantspermissiontotherequest.TheseHTTPverbsdonotmakechangestotherelatedresources,andtherefore,theyareincludedinthepermissions.SAFE_METHODStupleofstring.
IftheHTTPverbspecifiedintherequest(request.method)isnotanyofthethreesafemethods,thecodereturnsTrueandgrantspermissiononlywhentheownerattributeofthereceivedobj(obj.owner)matchestheuserthatcreatedtherequest(request.user).Thisway,onlytheowneroftherelatedresourcewillbegrantedpermissiontorequeststhatincludeHTTPverbsthataren'tsafe.
WewillusethenewIsOwnerOrReadOnlypermissionclasstomakesurethatonlythegameownerscanmakechangestoanexistinggame.Wewillcombinethispermissionclasswiththerest_framework.permissions.IsAuthenticatedOrReadOnlypermissionclassthatonlyallowsread-onlyaccesstoresourceswhentherequestisnotauthenticatedasauser.
PersistingtheuserthatmakesarequestWewanttobeabletolistalltheusersandretrievethedetailsforasingleuser.Wewillcreatesubclassesofthetwofollowinggenericclassviewsdeclaredinrest_framework.generics:
ListAPIView:ImplementsthegetmethodthatretrievesalistingofaquerysetRetrieveAPIView:Implementsthegetmethodtoretrieveamodelinstance
Gotothegamesapi/gamesfolderandopentheviews.pyfile.Addthefollowingcodeafterthelastlinethatdeclarestheimports,beforethedeclarationoftheGameCategoryListclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
fromdjango.contrib.auth.modelsimportUser
fromgames.serializersimportUserSerializer
fromrest_frameworkimportpermissions
fromgames.permissionsimportIsOwnerOrReadOnly
classUserList(generics.ListAPIView):
queryset=User.objects.all()
serializer_class=UserSerializer
name='user-list'
classUserDetail(generics.RetrieveAPIView):
queryset=User.objects.all()
serializer_class=UserSerializer
name='user-detail'
AddthefollowinghighlightedlinestotheApiRootclassdeclaredintheviews.pyfile.Now,wewillbeabletonavigatetotheuser-relatedviewsthroughoutthebrowsableAPI.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder.
classApiRoot(generics.GenericAPIView):
name='api-root'
defget(self,request,*args,**kwargs):
returnResponse({
'players':reverse(PlayerList.name,request=request),
'game-categories':reverse(GameCategoryList.name,request=request),
'games':reverse(GameList.name,request=request),
'scores':reverse(PlayerScoreList.name,request=request),
'users':reverse(UserList.name,request=request),
})
Gotothegamesapi/gamesfolderandopentheurls.pyfile.Addthefollowingelementstotheurlpatternsstringlist.ThenewstringsdefinetheURLpatternsthatspecifytheregularexpressionsthathavetobematchedintherequesttorunaspecificmethodforthepreviouslycreatedclassbased-viewsintheviews.pyfile:UserListandUserDetail.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
url(r'^users/$',
views.UserList.as_view(),
name=views.UserList.name),
url(r'^users/(?P<pk>[0-9]+)/$',
views.UserDetail.as_view(),
name=views.UserDetail.name),
Wehavetoaddalineintheurls.pyfileinthegamesapifolder,specifically,thegamesapi/urls.pyfile.ThefiledefinestherootURLconfigurationsandwewanttoincludetheURLpatternstoallowthebrowsableAPItodisplaytheloginandlogoutviews.Thefollowinglinesshowthenewcodeforthegamesapi/urls.pyfile.Thenewlineishighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
fromdjango.conf.urlsimporturl,include
urlpatterns=[
url(r'^',include('games.urls')),
url(r'^api-auth/',include('rest_framework.urls'))
]
WehavetomakechangestotheGameListclass-basedview.Wewilloverridetheperform_createmethodtopopulatetheownerbeforeanewGameinstanceispersistedinthedatabase.ThefollowinglinesshowthenewcodefortheGameListclassintheviews.pyfile.Thenewlinesarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
classGameList(generics.ListCreateAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-list'
defperform_create(self,serializer):
#Passanadditionalownerfieldtothecreatemethod
#ToSettheownertotheuserreceivedintherequest
serializer.save(owner=self.request.user)
TheGameListclassinheritstheperform_createmethodfromtherest_framework.mixins.CreateModelMixinclass.Rememberthatthegenerics.ListCreateAPIViewclassinheritsfromCreateModelMixinclassandotherclasses.Thecodefortheoverriddenperform_createmethodpassesanadditionalownerfieldtothecreatemethodbysettingavaluefortheownerargumentforthecalltotheserializer.savemethod.Thecodesetstheownerattributetothevalueofself.request.user,thatis,totheuserassociatedtotherequest.Thisway,wheneveranewgameispersisted,itwillsavetheuserassociatedtotherequestasitsowner.
ConfiguringpermissionpoliciesNow,wewillconfigurepermissionpoliciesfortheclass-basedviewsrelatedtogames.Wewilloverridethevalueforthepermission_classesclassattributefortheGameListandGameDetailclasses.
ThefollowinglinesshowthenewcodefortheGameListclassintheviews.pyfile.Thenewlinesarehighlighted.Don'tremovethecodeweaddedfortheperform_createmethodforthisclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
classGameList(generics.ListCreateAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-list'
permission_classes=(
permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,
)
ThefollowinglinesshowthenewcodefortheGameDetailclassintheviews.pyfile.Thenewlinesarehighlighted.Don'tremovethecodeweaddedfortheperform_createmethodforthisclass.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
classGameDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-detail'
permission_classes=(
permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly)
Weaddedthesamelinesinthetwoclasses.WehaveincludedtheIsAuthenticatedOrReadOnlyclassandourpreviouslycreatedIsOwnerOrReadOnlypermissionclassinthepermission_classestuple.
SettingadefaultvalueforanewrequiredfieldinmigrationsWehavepersistedmanygamesinourdatabaseandaddedanewownerfieldforthegamesthatisarequiredfield.Wedon'twanttodeletealltheexistinggames,andtherefore,wewilltakeadvantageofsomefeaturesinDjangothatmakeiteasyforustomakethechangesintheunderlyingdatabasewithoutlosingtheexistingdata.
Now,weneedtoretrievetheidforthesuperuserwehavecreatedtouseitasthedefaultownerfortheexistinggames.Djangowillallowustoeasilyupdatetheexistinggamestosettheowneruserforthem.
Runthefollowingcommandstoretrievetheidfromtheauth_usertablefortherowthatwhoseusernameisequalto'superuser'.Replacesuperuserwiththeusernameyouselectedforthepreviouslycreatedsuperuser.Inaddition,replaceuser_nameinthecommandwiththeusernameyouusedtocreatethePostgreSQLdatabaseandpasswordwithyourchosenpasswordforthisdatabaseuser.ThecommandassumesthatyouarerunningPostgreSQLonthesamecomputerinwhichyouarerunningthecommand.IncaseyouareworkingwithaSQLitedatabase,youcanruntheequivalentcommandinthePostgreSQLcommandlineoraGUI-basedtooltoexecutethesamequery.
psql--username=user_name--dbname=games--command="SELECTidFROMauth_user
WHEREusername='superuser';"
Thefollowinglinesshowtheoutputwiththevalueforid:1
id
----
1
(1row)
Now,runthefollowingPythonscripttogeneratethemigrationsthatwillallowustosynchronizethedatabasewiththenewfieldweaddedtotheGamemodel:
pythonmanage.pymakemigrationsgames
Djangowilldisplaythefollowingquestion:
Youaretryingtoaddanon-nullablefield'owner'togamewithoutadefault;
wecan'tdothat(thedatabaseneedssomethingtopopulateexistingrows).
Pleaseselectafix:
1)Provideaone-offdefaultnow(willbesetonallexistingrows)
2)Quit,andletmeaddadefaultinmodels.py
Selectanoption:
Wewanttoprovidetheone-offdefaultthatwillbesetonallexistingrows,andtherefore,enter1toselectthefirstoptionandpressEnter.
Djangowilldisplaythefollowingtextaskingustoenterthedefaultvalue:
Pleaseenterthedefaultvaluenow,asvalidPython
Thedatetimeanddjango.utils.timezonemodulesareavailable,soyoucando
e.g.timezone.now()
>>>
Enterthevalueforthepreviouslyretrievedid,1inourexample,andpressEnter.Thefollowinglinesshowtheoutputgeneratedafterrunningtheprecedingcommand:
Migrationsfor'games':
0003_game_owner.py:
-Addfieldownertogame
Theoutputindicatesthatthegamesapi/games/migrations/0003_game_owner.pyfileincludesthecodetoaddthefieldnamedownertogame.ThefollowinglinesshowthecodeforthisfilethatwasautomaticallygeneratedbyDjango.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder:
#-*-coding:utf-8-*-
#GeneratedbyDjango1.9.7on2016-07-0121:06
from__future__importunicode_literals
fromdjango.confimportsettings
fromdjango.dbimportmigrations,models
importdjango.db.models.deletion
classMigration(migrations.Migration):
dependencies=[
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('games','0002_auto_20160623_2131'),
]
operations=[
migrations.AddField(
model_name='game',
name='owner',
field=models.ForeignKey(default=1,
on_delete=django.db.models.deletion.CASCADE,related_name='games',
to=settings.AUTH_USER_MODEL),
preserve_default=False,
),
]
Thecodedeclaresasubclassofthedjango.db.migrations.MigrationclassnamedMigrationthatdefinesanoperationslistwithamigrations.AddFieldthatwilladdthetheownerfieldtothetablerelatedtothegamemodel.
Now,runthefollowingpythonscripttoapplyallthegeneratedmigrationsandexecutethechangesinthedatabasetables:
pythonmanage.pymigrate
Thefollowinglinesshowtheoutputgeneratedafterrunningthepreviouscommand.Notethattheorderingforthemigrationsmightbedifferentinyourconfiguration:
Operationstoperform:
Applyallmigrations:admin,auth,contenttypes,games,sessions
Runningmigrations:
Renderingmodelstates...DONE
Applyinggames.0003_game_owner...OK
Afterwerunthepreviouscommand,wewillhaveanewowner_idfieldaddedtothegames_gametableinthePostgreSQLdatabase.Theexistingrowsinthegames_gametablewillusethedefaultvalueweindicatedDjangotouseforthenewowner_idfield.WecanusethePostgreSQLcommandlineoranyotherapplicationthatallowsustoeasilycheckthecontentsofthePostreSQLdatabasetocheckthegames_gametablethatDjangoupdated.IncaseyoudecidetocontinueworkingwithSQLite,usethecommandsortoolsrelatedtothisdatabase.
Runthefollowingcommandtolaunchtheinteractiveshell.MakesureyouarewithinthegamesapifolderintheTerminalorCommandPrompt:
pythonmanage.pyshell
Youwillnoticethatalinethatsays(InteractiveConsole)isdisplayedaftertheusuallinesthatintroduceyourdefaultPythoninteractiveshell.EnterthefollowingcodeinthePythoninteractivetocreateanotheruserthatisnotasuperuser.Wewillusethisuserandthesuperusertotestourchangesinthepermissionspolicies.Thecodefileforthesampleisincludedintherestful_python_chapter_03_04folder,intheusers_test_01.pyfile.
Youcanreplacekevinwithyourdesiredusername,kevin@eaxmple.comwiththee-mailandkevinpasswordwiththepasswordyouwanttouseforthisuser.However,takeintoaccountthatwewillbeusingthesecredentialsinthefollowingsections.Makesureyoualwaysreplacethecredentialswithyourowncredentials:
fromdjango.contrib.auth.modelsimportUser
user=User.objects.create_user('kevin','[email protected]','kevinpassword')
user.save()
Finally,quittheinteractiveconsolebyenteringthefollowingcommand:
quit()
Now,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequests.ExecuteanyofthefollowingtwocommandsbasedonyourneedstoaccesstheAPIinotherdevicesorcomputersconnectedtoyourLAN.RememberthatweanalyzedthedifferencebetweentheminChapter1,DevelopingRESTfulAPIswithDjango:
pythonmanage.pyrunserver
pythonmanage.pyrunserver0.0.0.0:8000
Afterwerunanyoftheprecedingcommands,thedevelopmentserverwillstartlisteningatport8000.
ComposingrequestswiththenecessaryauthenticationNow,wewillcomposeandsendanHTTPrequesttocreateanewgamewithoutauthenticationcredentials:
httpPOST:8000/games/name='TheLastofUs'game_category='3DRPG'
played=falserelease_date='2016-06-21T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"TheLastof
Us","game_category":"3DRPG","played":"false","release_date":"2016-06-
21T03:02:00.776594Z"}':8000/games/
Wewillreceivea401Unauthorizedstatuscodeintheresponseheaderandadetailmessageindicatingthatwedidn'tprovideauthenticationcredentialsintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0401Unauthorized
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Sun,03Jul201622:23:07GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
WWW-Authenticate:Basicrealm="api"
X-Frame-Options:SAMEORIGIN
{
"detail":"Authenticationcredentialswerenotprovided."
}
Ifwewanttocreateanewgame,thatis,tomakeaPOSTrequestto/games/,weneedtoprovideauthenticationcredentialsusingHTTPauthentication.Now,wewillcomposeandsendanHTTPrequesttocreateanewgamewithauthenticationcredentials,thatis,withthesuperusernameandhispassword.Remembertoreplacesuperuserwiththenameyouusedforthesuperuserandpasswordwiththepasswordyouconfiguredforthisuser:
http-asuperuser:'password'POST:8000/games/name='TheLastofUs'
game_category='3DRPG'played=falserelease_date='2016-06-
21T03:02:00.776594Z'
Thefollowingistheequivalentcurlcommand:
curl--usersuperuser:'password'-iXPOST-H"Content-Type:application/json"
-d'{"name":"TheLastofUs","game_category":"3DRPG","played":"false",
"release_date":"2016-06-21T03:02:00.776594Z"}':8000/games/
IfthenewGamewiththesuperuseruserasitsownerwassuccessfullypersistedinthedatabase,thefunctionreturnsanHTTP201CreatedstatuscodeandtherecentlypersistedGame
serializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewGameobjectintheJSONresponse:
HTTP/1.0201Created
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Mon,04Jul201602:45:36GMT
Location:http://localhost:8000/games/16/
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept
X-Frame-Options:SAMEORIGIN
{
"game_category":"3DRPG",
"name":"TheLastofUs",
"owner":"superuser",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/16/"
}
Now,wewillcomposeandsendanHTTPrequesttoupdatetheplayedfieldvalueforthepreviouslycreatedgamewithauthenticationcredentials.However,inthiscase,wewillusetheotheruserwecreatedinDjangotoauthenticatetherequest.Remembertoreplacekevinwiththenameyouusedfortheuserandkevinpasswordwiththepasswordyouconfiguredforthisuser.Inaddition,replace16withtheidgeneratedforthepreviouslycreatedgameinyourconfiguration.WewillusethePATCHmethod.
http-akevin:'kevinpassword'PATCH:8000/games/16/played=true
Thefollowingistheequivalentcurlcommand:
curl--userkevin:'kevinpassword'-iXPATCH-H"Content-Type:
application/json"-d'{"played":"true"}':8000/games/16/
Wewillreceivea403ForbiddenstatuscodeintheresponseheaderandadetailmessageindicatingthatwedonothavepermissiontoperformtheactionintheJSONbody.Theownerforthegamewewanttoupdateissuperuserandtheauthenticationcredentialsforthisrequestuseadifferentuser.Thus,theoperationisrejectedbythehas_object_permissionmethodintheIsOwnerOrReadOnlyclass.Thefollowinglinesshowasampleresponse:
HTTP/1.0403Forbidden
Allow:GET,PUT,PATCH,DELETE,HEAD,OPTIONS
Content-Type:application/json
Date:Mon,04Jul201602:59:15GMT
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept
X-Frame-Options:SAMEORIGIN
{
"detail":"Youdonothavepermissiontoperformthisaction."
}
IfwecomposeandsendanHTTPrequestwiththesameauthenticationcredentialsforthe
sameresourcewiththeGETmethod,wewillbeabletoretrievethegamethatthespecifieduserdoesn'town.ItwillworkbecauseGETisoneofthesafemethodsandauserthatisnottheownerisallowedtoreadtheresource.Remembertoreplacekevinwiththenameyouusedfortheuserandkevinpasswordwiththepasswordyouconfiguredforthisuser.Inaddition,replace16withtheidgeneratedforthepreviouslycreatedgameinyourconfiguration:
http-akevin:'kevinpassword'GET:8000/games/16/
Thefollowingistheequivalentcurlcommand:
curl--userkevin:'kevinpassword'-iXGET:8000/games/16/
BrowsingtheAPIwithauthenticationcredentialsOpenawebbrowserandenterhttp://localhost:8000/.ReplacelocalhostbytheIPofthecomputerthatisrunningtheDjangodevelopmentserverincaseyouuseanothercomputerordevicetorunthebrowser.ThebrowsableAPIwillcomposeandsendaGETrequestto/andwilldisplaytheresultsofitsexecution,thatis,theApiRoot.YouwillnoticethatthereisaLoginhyperlinkintheupper-rightcorner.
ClickLoginandthebrowserwilldisplaytheDjangoRESTFrameworkloginpage.Enterkevininusername,kevinpasswordinpassword,andclickLogIn.Remembertoreplacekevinwiththenameyouusedfortheuserandkevinpasswordwiththepasswordyouconfiguredforthisuser.Now,youwillbeloggedinaskevinandalltherequestsyoucomposeandsendthroughthebrowsableAPIwillusethisuser.YouwillberedirectedagaintotheApiRootandyouwillnoticetheLogInhyperlinkisreplacedwiththeusername(kevin)andadrop-downmenuthatallowsyoutoLogOut.ThefollowingscreenshotshowstheApiRootafterweareloggedinaskevin.
ClickortapontheURLontheright-handsideofusers.Incaseyouarebrowsinginlocalhost,theURLwillbehttp://localhost:8000/users/.TheBrowsableAPIwillrenderthewebpagefortheUsersList.ThefollowinglinesshowtheJSONbodywiththefirstlinesandthelastlineswiththeresultsfortheGETrequesttolocalhost:8000/users/.
ThegamesarrayincludestheURLandthenameforeachgamethattheuserownsbecausetheUserGameSerializerclassisserializingthecontentforeachgame:
HTTP200OK
Allow:GET,HEAD,OPTIONS
Content-Type:application/json
Vary:Accept
{
"count":2,
"next":null,
"previous":null,
"results":[
{
"url":"http://localhost:8000/users/1/",
"pk":1,
"username":"superuser",
"games":[
{
"url":"http://localhost:8000/games/10/",
"name":"ADarkRoom"
},
{
"url":"http://localhost:8000/games/11/",
"name":"Bastion"
},
...
]
},
{
"url":"http://localhost:8000/users/3/",
"pk":3,
"username":"kevin",
"games":[]
}
]
}
ClickortapononeoftheURLsforthegameslistedasownedbythesuperuseruser.TheBrowsableAPIwillrenderthewebpagefortheGameDetail.ClickortaponOPTIONSandtheDELETEbuttonwillappear.ClickortaponDELETE.Thewebbrowserwilldisplayaconfirmationdialogbox.ClickortaponDELETE.Wewillreceivea403ForbiddenstatuscodeintheresponseheaderandadetailmessageindicatingthatwedonothavepermissiontoperformtheactionintheJSONbody.
Theownerforthegamewewanttodeleteissuperuserandtheauthenticationcredentialsforthisrequestuseadifferentuser,specifically,kevin.Thus,theoperationisrejectedbythehas_object_permissionmethodintheIsOwnerOrReadOnlyclass.Thefollowingscreenshotshowsasampleresponse:
Tip
WecanalsotakeadvantageofotherauthenticationpluginsthatDjangoRESTFrameworkprovidesus.Youcanreadmoreaboutallthepossibilitiesthattheframeworkprovidesusforauthenticationathttp://www.django-rest-framework.org/api-guide/authentication/
Testyourknowledge1. WhichisthemostappropriateHTTPmethodtoupdateasinglefieldforanexisting
resource:1. PUT2. POST3. PATCH
2. Whichofthefollowingpaginationclassesprovidesalimit/offsetbasedstyleinDjangoRESTFramework:1. rest_framework.pagination.LimitOffsetPagination2. rest_framework.pagination.LimitOffsetPaging3. rest_framework.styles.LimitOffsetPagination
3. Therest_framework.authentication.BasicAuthenticationclass:1. WorkswithDjango'ssessionframeworkforauthentication.2. ProvidesanHTTPBasicauthenticationagainstusernameandpassword.3. Providesasimpletokenbasedauthentication.
4. Therest_framework.authentication.SessionAuthenticationclass:1. WorkswithDjango'ssessionframeworkforauthentication.2. ProvidesanHTTPBasicauthenticationagainstusernameandpassword.3. Providesasimpletokenbasedauthentication.
5. Thevalueofwhichofthefollowingsettingskeysspecifyaglobalsettingwithatupleofstringwhosevaluesindicatetheclassesthatwewanttouseforauthentication:1. DEFAULT_AUTH_CLASSES2. AUTHENTICATION_CLASSES3. DEFAULT_AUTHENTICATION_CLASSES
SummaryInthischapter,weimprovedtheRESTAPIinmanyways.Weaddeduniqueconstraintstothemodelandupdatedthedatabase,wemadeiteasytoupdatesinglefieldswiththePATCHmethodandwetookadvantageofpagination.
Then,westartedworkingwithauthentication,permissions,andthrottling.Weaddedsecurity-relateddatatothemodelsandweupdatedthedatabase.WemadenumerouschangesinthedifferentpiecesofcodetoachieveaspecificsecuritygoalandwetookadvantageofDjangoRESTFrameworkauthenticationandpermissionsfeatures.
NowthatwehavebuiltanimprovedandcomplexAPIthattakesintoaccountauthenticationandusespermissionpolicies,wewilluseadditionalabstractionsincludedintheframework,wewilladdthrottlingandtests,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter4.Throttling,Filtering,Testing,andDeployinganAPIwithDjangoInthischapter,wewillusetheadditionalfeaturesincludedinDjangoandDjangoRESTFrameworktoimproveourRESTfulAPI.Wewillalsowriteandexecuteunittestsandlearnafewthingsrelatedtodeployment.Wewillcoverthefollowingtopicsinthischapter:
UnderstandingthrottlingclassesConfiguringthrottlingpoliciesTestingthrottlepoliciesUnderstandingfiltering,searchingandorderingclassesConfiguringfiltering,searching,andorderingforviewsTestingfiltering,searchingandorderingfeaturesFilter,search,andorderinthebrowsableAPIWritingafirstroundofunittestsRunningunittestsandcheckingtestingcoverageImprovingtestingcoverageUnderstandingstrategiesfordeploymentsandscalability
UnderstandingthrottlingclassesSofar,wehaven'testablishedanylimitsontheusageofourAPI,andtherefore,bothauthenticatedandunauthenticateduserscancomposeandsendasmanyrequestsastheywantto.WeonlytookadvantageofthepaginationfeaturesavailableinDjangoRESTFrameworktospecifyhowwewantedlargeresultssetstobesplitintoindividualpagesofdata.However,anyusercancomposeandsendthousandsofrequeststobeprocessedwithoutanykindoflimitation.
WewillusethrottlingtoconfigurethefollowinglimitationsoftheusageofourAPI:
Unauthenticatedusers:Amaximumoffiverequestsperhour.Authenticatedusers:Amaximumof20requestsperhour.
Inaddition,wewanttoconfigureamaximumof100requestsperhourtothegamecategoriesrelatedviews,nomatterwhethertheuserisauthenticatedornot.
DjangoRESTFrameworkprovidesthefollowingthreethrottlingclassesintherest_framework.throttlingmodule.AllofthemaresubclassesoftheSimpleRateThrottleclass,whichisasubclassoftheBaseThrottleclass.Theclassesallowustosetthemaximumnumberofrequestsperperiodthatarecomputedbasedondifferentmechanismstodeterminethepreviousrequestinformationusedtospecifythescope.Thepreviousrequestinformationforthrottlingisstoredinthecacheandtheclassesoverridetheget_cache_keymethodthatdeterminesthescope.
AnonRateThrottle:Thisclasslimitstherateofrequestthatananonymoususercanmake.TheIPaddressoftherequestistheuniquecachekey,andtherefore,alltherequestscomingfromthesameIPaddresswillaccumulatethetotalnumberofrequests.UserRateThrottle:Thisclasslimitstherateatwhichaspecificusercanmakerequests.Forauthenticatedusers,theauthenticateduserIDistheuniquecachekey.Foranonymoususers,theIPaddressoftherequestistheuniquecachekey.ScopedRateThrottle:ThisclasslimitstherateofrequestforspecificpartsoftheAPIidentifiedwiththevalueassignedtothethrottle_scopeproperty.TheclassisusefulwhenwewanttorestrictaccesstospecificpartsoftheAPIwithdifferentrates.
ConfiguringthrottlingpoliciesWewilluseacombinationofthethreethrottlingclasses,discussedearlier,toachieveourpreviouslyexplainedgoals.MakesureyouquitDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheTerminalorCommandPromptwindowinwhichitisrunning.
Openthegamesapi/settings.pyfileandaddthehighlightedlinestothedictionarynamedREST_FRAMEWORKwithtwokey-valuepairsthatconfiguretheglobaldefaultthrottlingclassesandtheirrates.Thecodefileforthesampleisincludedintherestful_python_chapter_04_01folder:
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS':
'games.pagination.LimitOffsetPaginationWithMaxLimit',
'PAGE_SIZE':5,
'DEFAULT_AUTHENTICATION_CLASSES':(
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_THROTTLE_CLASSES':(
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
),
'DEFAULT_THROTTLE_RATES':{
'anon':'5/hour',
'user':'20/hour',
'game-categories':'30/hour',
}
}
ThevaluefortheDEFAULT_THROTTLE_CLASSESsettingskeyspecifiesaglobalsettingwithatupleofstringwhosevaluesindicatethedefaultclassesthatwewanttouseforthrottling-AnonRateThrottleandUserRateThrottle.TheDEFAULT_THROTTLE_RATESsettingskeyspecifiesadictionarywithdefaultthrottlerates.Thevaluespecifiedforthe'anon'keyindicatesthatwewantamaximumoffiverequestsperhourforanonymoususers.Thevaluespecifiedforthe'user'keyindicatesthatwewantamaximumof20requestsperhourforauthenticatedusers.Thevaluespecifiedforthe'game-categories'keyindicatesthatwewantamaximumof30requestsperhourforthescopewiththatname.
Themaximumrateisastringthatspecifiesthenumberofrequestsperperiodwiththefollowingformat:'number_of_requests/period',whereperiodcanbeanyofthefollowing:
s:secondsec:secondm:minutemin:minuteh:hour
hour:hourd:dayday:day
Now,wewillconfigurethrottlingpoliciesfortheclass-basedviewsrelatedtogamecategories.Wewilloverridethevalueforthethrottle_scopeandthrottle_classesclassattributesfortheGameCategoryListandGameCategoryDetailclasses.First,wehavetoaddthefollowingimportstatementafterthelastimportintheviews.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_04_01folder:
fromrest_framework.throttlingimportScopedRateThrottle
ThefollowinglinesshowthenewcodefortheGameCategoryListclassintheviews.pyfile.Thenewlinesarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_04_01folder:
classGameCategoryList(generics.ListCreateAPIView):
queryset=GameCategory.objects.all()
serializer_class=GameCategorySerializer
name='gamecategory-list'
throttle_scope='game-categories'
throttle_classes=(ScopedRateThrottle,)
ThefollowinglinesshowthenewcodefortheGameCategoryDetailclassintheviews.pyfile.Thenewlinesarehighlightedinthefollowingcode.Thecodefileforthesampleisincludedintherestful_python_chapter_04_01folder:
classGameCategoryDetail(generics.RetrieveUpdateDestroyAPIView):
queryset=GameCategory.objects.all()
serializer_class=GameCategorySerializer
name='gamecategory-detail'
throttle_scope='game-categories'
throttle_classes=(ScopedRateThrottle,)
Weaddedthesamelinesinthetwoclasses.Weset'game-categories'asthevalueforthethrottle_scopeclassattributeandweincludedScopedRateThrottleinthetuplethatdefinesthevalueforthrottle_classes.Thisway,thetwoclass-basedviewswillusethesettingsspecifiedforthe'game-categories'scopeandtheScopeRateThrottleclassforthrottling.Theseviewswillbeabletoserve30requestsperhourandwon'ttakeintoaccounttheglobalsettingsthatapplytothedefaultclassesthatweuseforthrottling:AnonRateThrottleandUserRateThrottle.
BeforeDjangorunsthemainbodyofaview,itperformsthechecksforeachthrottleclassspecifiedinthethrottleclasses.Intheviewsrelatedtothegamecategories,wewrotecodethatoverridesthedefaultsettings.Ifasinglethrottlecheckfails,thecodewillraiseaThrottledexceptionandDjangowon'texecutethemainbodyoftheview.Thecacheisresponsibleofstoringpreviousrequests'informationforthrottlingchecking.
TestingthrottlingpoliciesNow,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequests.ExecuteanyofthefollowingtwocommandsbasedonyourneedstoaccesstheAPIinotherdevicesorcomputersconnectedtoyourLAN.RememberthatweanalyzedthedifferencebetweentheminChapter1,DevelopingRESTfulAPIswithDjango.
pythonmanage.pyrunserver
pythonmanage.pyrunserver0.0.0.0:8000
Afterwerunanyofthepreviouscommands,thedevelopmentserverwillstartlisteningatport8000.
Now,wewillcomposeandsendanHTTPrequesttoretrievealltheplayer'sscoreswithoutauthenticationcredentialssixtimes:
http:8000/player-scores/
WecanalsousethefeaturesoftheshellinmacOSorLinuxtorunthepreviouscommandsixtimeswithjustasingleline.WecanalsorunthecommandinaCygwinterminalinWindows.Wecanexecutethenextlineinabashshell.However,wewillseealltheresultsoneaftertheotherandyouwillhavetoscrolltounderstandwhathappenedwitheachexecution:
foriin{1..6};dohttp:8000/player-scores/;done;
Thefollowingistheequivalentcurlcommandthatwemustexecutesixtimes:
curl-iXGET:8000/player-scores/
ThefollowingistheequivalentcurlcommandthatisexecutedsixtimeswithasinglelineinabashshellinmacOSorLinux,oraCygwinterminalinWindows:
foriin{1..6};docurl-iXGET:8000/player-scores/;done;
Djangowon'tprocessthesixthrequestbecauseAnonRateThrottleisconfiguredasoneofthedefaultthrottleclassesanditsthrottlesettingsspecifyfiverequestsperhour.Thus,wewillreceivea429Toomanyrequestsstatuscodeintheresponseheaderandamessageindicatingthattherequestwasthrottledandthetimeinwhichtheserverwillbeabletoprocessanadditionalrequest.TheRetry-Afterkeyintheresponseheaderprovidesthenumberofsecondsthatitisnecessarytowaituntilthenextrequest:3189.Thefollowinglinesshowasampleresponse:
HTTP/1.0429TooManyRequests
Allow:GET,POST,HEAD,OPTIONS
Content-Type:application/json
Date:Tue,05Jul201603:37:50GMT
Retry-After:3189
Server:WSGIServer/0.2CPython/3.5.1
Vary:Accept,Cookie
X-Frame-Options:SAMEORIGIN
{
"detail":"Requestwasthrottled.Expectedavailablein3189seconds."
}
Now,wewillcomposeandsendanHTTPrequesttoretrievetheplayer'sscoreswithauthenticationcredentials,thatis,withthesuperusernameandhispassword.Wewillexecutethesamerequestsixtimes.RemembertoreplacesuperuserwiththenameyouusedforthesuperuserandpasswordwiththepasswordyouconfiguredforthisuserinChapter3,ImprovingandAddingAuthenticationtoanAPIwithDjango:
http-asuperuser:'password':8000/player-scores/
Wecanalsorunthepreviouscommandsixtimeswithjustasingleline:
foriin{1..6};dohttp-asuperuser:'password':8000/player-scores/;done;
Thefollowingistheequivalentcurlcommandthatwemustexecutesixtimes:
curl--usersuperuser:'password'-iXGET:8000/player-scores/
Thefollowingistheequivalentcurlcommandthatisexecutedsixtimeswithasingleline:
foriin{1..6};docurl--usersuperuser:'password'-iXGET:8000/player-
scores/;done;
Djangowillprocessthesixthrequestbecausewehavecomposedandsentsixauthenticatedrequestswiththesameuser,UserRateThrottleisconfiguredasoneofthedefaultthrottleclassesanditsthrottlesettingsspecify20requestsperhour.
Ifwerunthepreviouscommands15timesmore,wewillaccumulate21requestsandwewillwillreceivea429Toomanyrequestsstatuscodeintheresponseheaderandamessageindicatingthattherequestwasthrottledandthetimeinwhichtheserverwillbeabletoprocessanadditionalrequestafterthelastexecution.
Now,wewillcomposeandsendanHTTPrequesttoretrieveallthegamecategoriesthirtytimeswithouttheauthenticationcredentials:
http:8000/game-categories/
Wecanalsorunthepreviouscommandthirtytimeswithjustasingleline:
foriin{1..30};dohttp:8000/game-categories/;done;
Thefollowingistheequivalentcurlcommandthatwemustexecutethirtytimes:
curl-iXGET:8000/game-categories/
Thefollowingistheequivalentcurlcommandthatisexecutedthirtytimeswithasingleline:
foriin{1..30};docurl-iXGET:8000/game-categories/;done;
Djangowillprocessthethirtyrequestsbecausewehavecomposedandsent30unauthenticatedrequeststoaURLthatisidentifiedwiththe'game-categories'throttlescopeandusestheScopedRateThrottleclassforthrottlepermissioncontrol.Thethrottlesettingsforthethrottlescopeidentifiedwith'game-categories'areconfiguredwith30requestsperhour.
Ifwerunthepreviouscommandonceagain,wewillaccumulate31requestsandwewillreceivea429Toomanyrequestsstatuscodeintheresponseheaderandamessageindicatingthattherequestwasthrottledandthetimeinwhichtheserverwillbeabletoprocessanadditionalrequestafterthelastexecution.
Understandingfiltering,searching,andorderingclassesWetookadvantageofthepaginationfeaturesavailableinDjangoRESTFrameworktospecifyhowwewantedlargeresultssetstobesplitintoindividualpagesofdata.However,wehavealwaysbeenworkingwiththeentirequerysetastheresultset.DjangoRESTFrameworkmakesiteasytocustomizefiltering,searching,andsortingcapabilitiestotheviewswehavealreadycoded.
First,wewillinstallthedjango-filterpackageinourvirtualenvironment.Thisway,wewillbeabletousefieldfilteringfeaturesthatwecaneasilycustomizeinDjangoRESTFramework.MakesurethatyouquittheDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheterminalorCommandPromptwindowinwhichitisrunning.Then,wejustneedtorunthefollowingcommandtoinstallthedjango-filterpackage:
pipinstalldjango-filter
Thelastlinesfortheoutputwillindicatethatthedjango-filterpackagehasbeensuccessfullyinstalled.
Collectingdjango-filter
Downloadingdjango_filter-0.13.0-py2.py3-none-any.whl
Installingcollectedpackages:django-filter
Successfullyinstalleddjango-filter-0.13.0
Inaddition,wewillinstallthedjango-cripsy-formspackageinourvirtualenvironment.ThispackageenhanceshowthebrowsableAPIrendersthedifferentfilters.Runthefollowingcommandtoinstallthedjango-cripsy-formspackage:Wejustneedtorunthefollowingcommandtoinstallthispackage:
pipinstalldjango-crispy-forms
Thelastlinesfortheoutputwillindicatethatthedjango-crispy-formspackagehasbeensuccessfullyinstalled:
Collectingdjango-crispy-forms
Installingcollectedpackages:django-crispy-forms
Runningsetup.pyinstallfordjango-crispy-forms
Successfullyinstalleddjango-crispy-forms-1.6.0
Openthegamesapi/settings.pyfileandaddthehighlightedlinestotheREST_FRAMEWORKdictionary.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
REST_FRAMEWORK={
'DEFAULT_PAGINATION_CLASS':
'games.pagination.LimitOffsetPaginationWithMaxLimit',
'PAGE_SIZE':5,
'DEFAULT_FILTER_BACKENDS':(
'rest_framework.filters.DjangoFilterBackend',
'rest_framework.filters.SearchFilter',
'rest_framework.filters.OrderingFilter',
),
'DEFAULT_AUTHENTICATION_CLASSES':(
'rest_framework.authentication.BasicAuthentication',
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_THROTTLE_CLASSES':(
'rest_framework.throttling.AnonRateThrottle',
'rest_framework.throttling.UserRateThrottle',
),
'DEFAULT_THROTTLE_RATES':{
'anon':'5/hour',
'user':'20/hour',
'game-categories':'30/hour',
}
}
Thevalueforthe'DEFAULT_FILTER_BACKENDSsettingskeyspecifiesaglobalsettingwithatupleofstringwhosevaluesindicatethedefaultclassesthatwewanttouseforfilterbackends.Wewillusethefollowingthreeclasses:
rest_framework.filters.DjangoFilterBackend:Thisclassprovidesfieldfilteringcapabilities.Itusesthepreviouslyinstalleddjango-filterpackage.Wecanspecifythesetoffieldswewanttobeabletofilteragainstorcreatearest_framework.filters.FilterSetclasswithmorecustomizedsettingsandassociateitwiththeview.rest_framework.filters.SearchFilter:Thisclassprovidessinglequeryparameter-basedsearchingcapabilitiesanditisbasedonDjangoadmin'ssearchfunction.Wecanspecifythesetoffieldswewanttoincludeforthesearchandtheclientwillbeabletofilteritemsbymakingqueriesthatsearchthesefieldswithasinglequery.Itisusefulwhenwewanttomakeitpossibleforarequesttosearchmultiplefieldswithasinglequery.rest_framework.filters.OrderingFilter:Thisclassallowstheclienttocontrolhowtheresultsareorderedwithasingle-queryparameter.Wecanalsospecifythefieldsthatcanbeorderedagainst.
Tip
Wecanalsoconfigurethefilterbackendsbyincludinganyofthepreviouslyenumeratedclassesinatupleandassignittothefilter_backendsclassattributeforthegenericviewclasses.However,inthiscase,wewillusethedefaultconfigurationforallourclass-basedviews.
Add'crispy_forms'totheinstalledappsinthesettings.pyfile,specifically,totheINSTALLED_APPSstringlist.Thefollowingcodeshowsthelineswemustaddasthehighlightedcode.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#DjangoRESTFramework
'rest_framework',
#Gamesapplication
'games.apps.GamesConfig',
#Crispyforms
'crispy_forms',
]
Tip
Wehavetobecarefulwiththefieldsweconfiguretobeavailableinthefiltering,searching,andorderingfeatures.Theconfigurationwillhaveanimpactonthequeriesexecutedonthedatabase,andtherefore,wemustensurethatwehavetheappropriatedatabaseoptimizationsconsideringthequeriesthatwillbeexecuted.
Configuringfiltering,searching,andorderingforviewsGotothegamesapi/gamesfolderandopentheviews.pyfile.AddthefollowingcodeafterthelastlinethatdeclarestheimportsbutbeforethedeclarationoftheUserListclass.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
fromrest_frameworkimportfilters
fromdjango_filtersimportNumberFilter,DateTimeFilter,AllValuesFilter
AddthefollowinghighlightedlinestotheGameCategoryListclassdeclaredintheviews.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
classGameCategoryList(generics.ListCreateAPIView):
queryset=GameCategory.objects.all()
serializer_class=GameCategorySerializer
name='gamecategory-list'
throttle_scope='game-categories'
throttle_classes=(ScopedRateThrottle,)
filter_fields=('name',)
search_fields=('^name',)
ordering_fields=('name',)
Thefilter_fieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesthatwewanttobeabletofilteragainst.Underthehoods,DjangoRESTFrameworkwillautomaticallycreatearest_framework.filters.FilterSetclassandassociateittotheGameCategoryListview.Thisway,wewillbeabletofilteragainstthenamefield.
Thesearch_fieldsattributespecifiesatupleofstringwhosevaluesindicatethetext-typefieldnamesthatwewanttoincludeinthesearchfeature.Inthiscase,wewanttosearchonlyagainstthenamefieldandperformastarts-withmatch.The'^'includedasaprefixofthefieldnameindicatesthatwewanttorestrictthesearchbehaviortoastarts-withmatch.
Theordering_fieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesthattheclientcanspecifytosorttheresults.Incasetheclientdoesn'tspecifyafieldforordering,theresponsewillusethedefaultorderingfieldsindicatedinthemodelrelatedtotheview.
AddthefollowinghighlightedlinestotheGameListclassdeclaredintheviews.pyfile.Thenewlinesspecifythefieldstobeusedinthefilter,search,andorderingfeatures.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
classGameList(generics.ListCreateAPIView):
queryset=Game.objects.all()
serializer_class=GameSerializer
name='game-list'
permission_classes=(
permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly,
)
filter_fields=(
'name',
'game_category',
'release_date',
'played',
'owner',
)
search_fields=(
'^name',
)
ordering_fields=(
'name',
'release_date',
)
defperform_create(self,serializer):
serializer.save(owner=self.request.user)
Inthiscase,wespecifiedmanyfieldnamesinthefilter_fieldsattribute.Weincluded'game_category'and'owner'inthestringtuple,andtherefore,theclientwillbeabletoincludetheidvaluesforanyofthesetwofieldsinthefilter.Wewilltakeadvantageofotheroptionsforrelatedmodels,whichwilllaterallowustofiltertherelatedmodelsbyfield.Thisway,wewillunderstandthedifferentcustomizationsavailable.
Theordering_fieldsattributespecifiestwofieldnamesforthetupleofstring,andtherefore,theclientwillbeabletoordertheresultsbyeithernameorrelease_date.
AddthefollowinghighlightedlinestothePlayerListclassdeclaredintheviews.pyfile.Thenewlinesspecifythefieldstobeusedinthefilter,search,andorderingfeatures.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
classPlayerList(generics.ListCreateAPIView):
queryset=Player.objects.all()
serializer_class=PlayerSerializer
name='player-list'
filter_fields=(
'name',
'gender',
)
search_fields=(
'^name',
)
ordering_fields=(
'name',
)
AddthefollowinglinestocreatethenewPlayerScoreFilterclassintheviews.pyfilebutbeforethedeclarationofthePlayerScoreListclass.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
classPlayerScoreFilter(filters.FilterSet):
min_score=NumberFilter(
name='score',lookup_expr='gte')
max_score=NumberFilter(
name='score',lookup_expr='lte')
from_score_date=DateTimeFilter(
name='score_date',lookup_expr='gte')
to_score_date=DateTimeFilter(
name='score_date',lookup_expr='lte')
player_name=AllValuesFilter(
name='player__name')
game_name=AllValuesFilter(
name='game__name')
classMeta:
model=PlayerScore
fields=(
'score',
'from_score_date',
'to_score_date',
'min_score',
'max_score',
#player__namewillbeaccessedasplayer_name
'player_name',
#game__namewillbeaccessedasgame_name
'game_name',
)
ThePlayerScoreFilterisasubclassoftherest_framework.filters.FilterSetclass.WewanttocustomizesettingsforthefieldsthatwewilluseforfilteringinthePlayerScoreListclass-basedview,andtherefore,wecreatedthenewPlayerScoreFilterclass.Theclassdeclaresthefollowingsixclassattributes:
min_score:Itisadjango_filters.NumberFilterinstancethatallowstheclienttofiltertheplayerscoreswhosescorenumericvalueisgreaterthanorequaltothespecifiednumber.Thevaluefornameindicatesthefieldtowhichthenumericfilterisapplied,'score',andthelookup_exprvalueindicatesthelookupexpression,'gte',whichmeansgreaterthanorequalto.max_score:Itisadjango_filters.NumberFilterinstancethatallowstheclienttofiltertheplayerscoreswhosescorenumericvalueislessthanorequaltothespecifiednumber.Thevaluefornameindicatesthefieldtowhichthenumericfilterisapplied,'score',andthelookup_exprvalueindicatesthelookupexpression,'lte',whichmeanslessthanorequalto.from_score_date:Itisadjango_filters.DateTimeFilterinstancethatallowstheclienttofiltertheplayerscoreswhosescore_datedatetimevalueisgreaterthanorequaltothespecifieddatetimevalue.Thevaluefornameindicatesthefieldtowhichthedatetimefilterisapplied,'score_date',andthelookup_exprvalueindicatesthelookupexpression,'gte'.to_score_date:Itisadjango_filters.DateTimeFilterinstancethatallowstheclienttofiltertheplayerscoreswhosescore_datedatetimevalueislessthanorequaltothespecifieddatetimevalue.Thevaluefornameindicatesthefieldtowhichthedatetime
filterisapplied,'score_date',andthelookup_exprvalueindicatesthelookupexpression,'lte'.player_name:Itisadjango_filters.AllValuesFilter:Itisaninstancethatallowstheclienttofiltertheplayerscoreswhoseplayer'snamematchesthespecifiedstringvalue.Thevaluefornameindicatesthefieldtowhichthefilterisapplied,'player__name'.Notethatthevaluehasadoubleunderscore(__)andyoucanreaditasthenamefieldfortheplayermodelorsimplyreplacethedoubleunderscorewithadotandreadplayer.name.ThenameusesDjango'sdoubleunderscoresyntax.However,wedon'twanttheclienttouseplayer__nametospecifythefilterfortheplayer'sname.Thus,theinstanceisstoredintheclassattributenamedplayer_name,withjustasingleunderscorebetweenplayerandname.ThebrowsableAPIwilldisplayadropdownwithallthepossiblevaluesfortheplayer'snametouseasafilter.Thedropdownwillonlyincludetheplayers'namesthathaveregisteredscoresbecauseweusedtheAllValuesFilterclass.game_name:Thisisadjango_filters.AllValuesFilterinstancethatallowstheclienttofiltertheplayerscoreswhosegame'snamematchesthespecifiedstringvalue.Thevaluefornameindicatesthefieldonwhichthefilterisapplied,'game__name'.ThenameusesthepreviouslyexplainedDjango'sdoubleunderscoresyntax.Ashappenedwithplayer_name,wedon'twanttheclienttousegame__nametospecifythefilterforthegame'sname,andtherefore,westoredtheinstanceintheclassattributenamedgame_name,withjustasingleunderscorebetweengameandname.ThebrowsableAPIwilldisplayadropdownwithallthepossiblevaluesforthegame'snametouseasafilter.Thedropdownwillonlyincludethegame'snamesthathaveregisteredscoresbecauseweusedtheAllValuesFilterclass.
Inaddition,thePlayerScoreFilterclassdeclaresaMetainnerclassthatdeclarestwoattributes:modelandfields.Themodelattributespecifiesthemodelrelatedtothefilterset,thatis,thePlayerScoreclass.Thefieldsattributespecifiesatupleofstringwhosevaluesindicatethefieldnamesandfilternamesthatwewanttoincludeinthefiltersfortherelatedmodel.Weincluded'scores'andthenamesforallthepreviouslydeclaredfilters.Thestring'scores'referstothescorefieldnameandwewanttoapplythedefaultnumericfilterthatwillbebuiltunderthehoodstoallowtheclienttofilterbyanexactmatchonthescorefield.
Finally,addthefollowinghighlightedlinestothePlayerScoreListclassdeclaredintheviews.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_04_02folder:
classPlayerScoreList(generics.ListCreateAPIView):
queryset=PlayerScore.objects.all()
serializer_class=PlayerScoreSerializer
name='playerscore-list'
filter_class=PlayerScoreFilter
ordering_fields=(
'score',
'score_date',
)
Thefilter_classattributespecifiestheFilterSetsubclassthatwewanttouseforthisclass-
basedview:PlayerScoreFilter.Inaddition,wespecifiedthetwofieldnamesthattheclientwillbeabletousefororderingintheordering_fieldstupleofstring.
Testingfiltering,searching,andorderingNow,wecanlaunchDjango'sdevelopmentservertocomposeandsendHTTPrequests.ExecuteanyofthefollowingtwocommandsbasedonyourneedstoaccesstheAPIinotherdevicesorcomputersconnectedtoyourLAN.RememberthatweanalyzedthedifferencebetweentheminChapter1,DevelopingRESTfulAPIswithDjango.
pythonmanage.pyrunserver
pythonmanage.pyrunserver0.0.0.0:8000
Afterwerunanyofthepreviouscommands,thedevelopmentserverwillstartlisteningatport8000:
Now,wewillcomposeandsendanHTTPrequesttoretrieveallthegamecategorieswhosenamematches3DRPG:
http:8000/game-categories/?name=3D+RPG
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8000/game-categories/?name=3D+RPG
Thefollowinglinesshowasampleresponsewiththesinglegamecategorywhosenamematchesthespecifiednameinthefilter.ThefollowinglinesonlyshowtheJSONbodywithouttheheaders:
{
"count":1,
"next":null,
"previous":null,
"results":[
{
"games":[
"http://localhost:8000/games/2/",
"http://localhost:8000/games/15/",
"http://localhost:8000/games/3/",
"http://localhost:8000/games/16/"
],
"name":"3DRPG",
"pk":3,
"url":"http://localhost:8000/game-categories/3/"
}
]
}
WewillcomposeandsendanHTTPrequesttoretrieveallthegameswhoserelatedcategoryidisequalto3andthevaluefortheplayedfieldisequaltoTrue.Wewanttosorttheresultsbyrelease_dateindescendingorder,andtherefore,wespecify-release_dateinthevalueforordering.Thehyphen(-)beforethefieldnamespecifiestheorderingfeaturetousedescendingorderinsteadofthedefaultascendingorder.Makesureyoureplace3withthepk
valueofthepreviouslyretrievedgamecategorynamed3DRPG.Theplayedfieldisaboolfield,andtherefore,wehavetousePython-validboolvalues(TrueandFalse)whenspecifyingthedesiredvaluesfortheboolfieldinthefilter:
http':8000/games/?game_category=3&played=True&ordering=-release_date'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?game_category=3&played=True&ordering=-
release_date'
Thefollowinglinesshowasampleresponsewiththetwogamesthatmatchthespecifiedcriteriainthefilter.ThefollowinglinesonlyshowtheJSONbodywithouttheheaders:
{
"count":2,
"next":null,
"previous":null,
"results":[
{
"game_category":"3DRPG",
"name":"PvZGardenWarfare4",
"owner":"superuser",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/2/"
},
{
"game_category":"3DRPG",
"name":"SupermanvsAquaman",
"owner":"superuser",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/3/"
}
]
}
IntheGameListclass,wespecified'game_category'asoneofthestringsinthefilter_fieldstupleofstring.Thus,wehadtousethegamecategoryidinthefilter.Now,wewilluseafilteronthegame'snamerelatedtoaregisteredscore.ThePlayerScoreFilterclassprovidesusafiltertothenameoftherelatedgameingame_name.Wewillcombinethefilterwithanotherfilterontheplayer'snamerelatedtoaregisteredscore.ThePlayerScoreFilterclassprovidesusafiltertothenameoftherelatedplayerinplayer_name.Bothconditionsspecifiedinthecriteriamustbemet,andtherefore,thefiltersarecombinedwiththeANDoperator:
http':8000/player-scores/?player_name=Kevin&game_name=Superman+vs+Aquaman'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/player-scores/?
player_name=Kevin&game_name=Superman+vs+Aquaman'
Thefollowinglinesshowasampleresponsewiththescorethatmatchesthespecifiedcriteriainthefilters.ThefollowinglinesonlyshowtheJSONbodywithouttheheaders:
{
"count":1,
"next":null,
"previous":null,
"results":[
{
"game":"SupermanvsAquaman",
"pk":5,
"player":"Kevin",
"score":123200,
"score_date":"2016-06-22T03:02:00.776594Z",
"url":"http://localhost:8000/player-scores/5/"
}
]
}
WewillcomposeandsendanHTTPrequesttoretrieveallthescoresthatmatchthefollowingcriteria.Theresultswillbeorderedbyscore_dateindescendingorder.
Thescorevalueisbetween30,000and150,000Thescore_dateisbetween2016-06-21and2016-06-22
http':8000/player-scores/?score=&from_score_date=2016-06-
01&to_score_date=2016-06-28&min_score=30000&max_score=150000&ordering=-
score_date'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/player-scores/?score=&from_score_date=2016-06-
01&to_score_date=2016-06-28&min_score=30000&max_score=150000&ordering=-
score_date'
Thefollowinglinesshowasampleresponsewiththethreegamesthatmatchthespecifiedcriteriainthefilters.Weoverrodethedefaultorderingspecifiedinthemodelwiththespecifiedorderingintherequest.ThefollowinglinesonlyshowtheJSONbodywithouttheheaders:
{
"count":3,
"next":null,
"previous":null,
"results":[
{
"game":"SupermanvsAquaman",
"pk":5,
"player":"Kevin",
"score":123200,
"score_date":"2016-06-22T03:02:00.776594Z",
"url":"http://localhost:8000/player-scores/5/"
},
{
"game":"PvZGardenWarfare4",
"pk":4,
"player":"Brandon",
"score":85125,
"score_date":"2016-06-22T01:02:00.776594Z",
"url":"http://localhost:8000/player-scores/4/"
},
{
"game":"PvZGardenWarfare4",
"pk":3,
"player":"Brandon",
"score":35000,
"score_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/player-scores/3/"
}
]
}
Tip
Intheprecedingrequests,alltheresponsesdidn'thavemorethanonepage.Incasetheresponserequiresmorethanonepage,thevaluesforthepreviousandnextkeyswilldisplaytheURLsthatincludethecombinationofthefilters,search,orderingandpagination.
WewillcomposeandsendanHTTPrequesttoretrieveallthegameswhosenamestartswith'S'.Wewillusethesearchfeaturethatweconfiguredtorestrictthesearchbehaviortoastarts-withmatchonthenamefield:
http':8000/games/?search=S'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':8000/games/?search=S'
Thefollowinglinesshowasampleresponsewiththetwogamesthatmatchthespecifiedsearchcriteria,thatis,thosegameswhosenamestartswith'S'.ThefollowinglinesonlyshowtheJSONbodywithouttheheaders:
{
"count":2,
"next":null,
"previous":null,
"results":[
{
"game_category":"2Dmobilearcade",
"name":"ScribblenautsUnlimited",
"owner":"superuser",
"played":false,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/7/"
},
{
"game_category":"3DRPG",
"name":"SupermanvsAquaman",
"owner":"superuser",
"played":true,
"release_date":"2016-06-21T03:02:00.776594Z",
"url":"http://localhost:8000/games/3/"
}
]
}
Tip
Wecanchangethesearchandorderingparameter'sdefaultnames:'search'and'ordering'.WejustneedtospecifythedesirednamesintheSEARCH_PARAMandtheORDERING_PARAMsettings.
Filtering,searching,andorderingintheBrowsableAPIWecantakeadvantageofthebrowsableAPItoeasilytestfilter,search,andorderfeaturesthroughawebbrowser.Openawebbrowserandenterhttp://localhost:8000/player-scores/.Incaseyouuseanothercomputerordevicetorunthebrowser,replacelocalhostwiththeIPofthecomputerthatisrunningtheDjangodevelopmentserver.ThebrowsableAPIwillcomposeandsendaGETrequestto/player-scores/andwilldisplaytheresultsofitsexecution,thatis,theheadersandtheJSONplayerscoreslist.YouwillnoticethatthereisanewFiltersbuttonlocatedontheleft-handsideoftheOPTIONSbutton.
ClickonFiltersandthebrowsableAPIwilldisplaytheFiltersdialogboxwiththeappropriatecontrolsforeachfilterthatyoucanapplybelowFieldFiltersandthedifferentorderingoptionsbelowOrdering.ThefollowingscreenshotshowstheFiltersdialogbox:
BoththePlayernameandGamenamedropdownswillonlyincludetherelatedplayer'sandgame'snamesthathaveregisteredscoresbecauseweusedtheAllValuesFilterclassforbothfilters.Afterweenterallthevaluesforthefilters,wecanselectthedesiredorderingoptionorclickSubmit.ThebrowsableAPIwillcomposeandsendtheappropriateHTTPrequestand
willrenderawebpagewiththeresultsofitsexecution.TheresultswillincludetheHTTPrequestthatwasmadetotheDjangoserver.Thefollowingscreenshotshowsanexampleoftheresultofexecutingthenextrequest,thatis,therequestwebuiltusingthebrowsableAPI:
GET/player-scores/?
score=&from_score_date=&to_score_date=&min_score=30000&max_score=40000&player
_name=Brandon&game_name=PvZ+Garden+Warfare+4
SettingupunittestsFirst,wewillinstallthecoverageanddjango-nosepackagesinourvirtualenvironment.Wewillmakethenecessaryconfigurationstousethedjango_nose.NoseTestRunnerclasstorunallthetestswecodeandwewillusethenecessaryconfigurationstoimprovetheaccuracyofthetestcoveragemeasurements.
MakesurethatyouquitDjango'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheterminalortheCommandPromptwindowinwhichitisrunning.Wejustneedtorunthefollowingcommandtoinstallthecoveragepackage:
pipinstallcoverage
Thelastfewlinesoftheoutputindicatethatthedjango-nosepackagehasbeensuccessfullyinstalled:
Collectingcoverage
Downloadingcoverage-4.1.tar.gz
Installingcollectedpackages:coverage
Runningsetup.pyinstallforcoverage
Successfullyinstalledcoverage-4.1
Wejustneedtorunthefollowingcommandtoinstallthedjango-nosepackage:
pipinstalldjango-nose
Thelastfewlinesoftheoutputindicatethatthedjango-nosepackagehasbeensuccessfullyinstalled.
Collectingdjango-nose
Downloadingdjango_nose-1.4.4-py2.py3-none-any.whl
Collectingnose>=1.2.1(fromdjango-nose)
Downloadingnose-1.3.7-py3-none-any.whl
Installingcollectedpackages:nose,django-nose
Successfullyinstalleddjango-nose-1.4.4nose-1.3.7
Add'django_nose'totheinstalledappsinthesettings.pyfile,specifically,totheINSTALLED_APPSstringlist.Thefollowingcodeshowsthelinesweneedtoaddashighlightedcode.Thecodefileforthesampleisincludedintherestful_python_chapter_04_03folder:
INSTALLED_APPS=[
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
#DjangoRESTFramework
'rest_framework',
#Gamesapplication
'games.apps.GamesConfig',
#Crispyforms
'crispy_forms',
#Djangonose
'django_nose',
]
Openthegamesapi/settings.pyfileandaddthefollowinglinestoconfigurethedjango_nose.NoseTestRunnerclassasourtestrunnerandspecifythedefaultcommand-lineoptionsthatwewillusewhenwerunourtests.Thecodefileforthesampleisincludedintherestful_python_chapter_04_03folder:
#Wewanttousenosetorunallthetests
TEST_RUNNER='django_nose.NoseTestSuiteRunner'
#Wewantnosetomeasurecoverageonthegamesapp
NOSE_ARGS=[
'--with-coverage',
'--cover-erase',
'--cover-inclusive',
'--cover-package=games',
]
TheNOSE_ARGSsettingsspecifythefollowingcommand-lineoptionsforthenosetestsuiterunnerandforcoverage:
--with-coverage:Thisoptionspecifiesthatwealwayswanttogenerateatestcoveragereport.--cover-erase:Thisoptionmakessurethethetestrunnerdeletesthecoveragetestresultsfromthepreviousrun.--cover-inclusive:ThisoptionincludesallthePythonfilesundertheworkingdirectoryinthecoveragereport.Thisway,wemakesurethatwediscoverholesintestcoveragewhenwedon'timportallthefilesinourtestsuite.Wewillcreateatestsuitethatwon'timportallthefiles,andtherefore,thisoptionisveryimportanttohaveanaccuratetestcoveragereport.--cover-package=games:Thisoptionindicatesthemodulethatwewanttocover:games.
Finally,createanewtextfilenamed.coveragercwithinthegamesapirootfolderwiththefollowingcontent:
[run]
omit=*migrations*
Thisway,thecoverageutilitywon'ttakeintoaccountmanythingsrelatedtothegeneratedmigrationswhenprovidinguswiththetestcoveragereport.Wewillhaveamoreaccuratetestcoveragereportwiththissettingsfile.
WritingafirstroundofunittestsNow,wewillwritethefirstroundofunittests.Specifically,wewillwriteunittestsrelatedtothegamecategoryclass-basedviews:GameCategoryListandGameCategoryDetail.Opentheexistinggames/test.pyfileandreplacetheexistingcodewiththefollowinglinesthatdeclaremanyimportstatementsandtheGameCategoryTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_04_04folder,asshown:
fromdjango.testimportTestCase
fromdjango.core.urlresolversimportreverse
fromdjango.utils.httpimporturlencode
fromrest_frameworkimportstatus
fromrest_framework.testimportAPITestCase
fromgames.modelsimportGameCategory
classGameCategoryTests(APITestCase):
defcreate_game_category(self,name):
url=reverse('gamecategory-list')
data={'name':name}
response=self.client.post(url,data,format='json')
returnresponse
deftest_create_and_retrieve_game_category(self):
"""
EnsurewecancreateanewGameCategoryandthenretrieveit
"""
new_game_category_name='NewGameCategory'
response=self.create_game_category(new_game_category_name)
self.assertEqual(response.status_code,status.HTTP_201_CREATED)
self.assertEqual(GameCategory.objects.count(),1)
self.assertEqual(
GameCategory.objects.get().name,
new_game_category_name)
print("PK{0}".format(GameCategory.objects.get().pk))
TheGameCategoryTestsclassisasubclassofrest_framework.test.APITestCase.Theclassdeclaresthecreate_game_categorymethodthatreceivesthedesirednameforthenewgamecategoryasanargument.ThemethodbuildstheURLandthedatadictionarytocomposeandsendanHTTPPOSTmethodtotheviewassociatedwiththegamecategory-listviewnameandreturnstheresponsegeneratedbythisrequest.Thecodeusesself.clienttoaccesstheAPIClientinstancethatallowsustoeasilycomposeandsendHTTPrequestsfortesting.Inthiscase,thecodecallsthepostmethodwiththebuilturl,thedatadictionary,andthedesiredformatforthedata-'json'.Manytestmethodswillcallthecreate_game_categorymethodtocreateagamecategoryandthencomposeandsendotherHTTPrequeststotheAPI.
Thetest_create_and_retrieve_game_categorymethodtestswhetherwecancreateanewGameCategoryandthenretrieveit.Themethodcallsthecreate_game_categorymethodexplainedearlierandthenusesassertEqualtocheckforthefollowingexpectedresults:
Thestatus_codefortheresponseisHTTP201Created(status.HTTP_201_CREATED)ThetotalnumberofGameCategoryobjectsretrievedfromthedatabaseis1
AddthefollowingmethodstotheGameCategoryTestsclasswecreatedinthegames/test.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_04_04folder:
deftest_create_duplicated_game_category(self):
"""
EnsurewecancreateanewGameCategory.
"""
url=reverse('gamecategory-list')
new_game_category_name='NewGameCategory'
data={'name':new_game_category_name}
response1=self.create_game_category(new_game_category_name)
self.assertEqual(
response1.status_code,
status.HTTP_201_CREATED)
response2=self.create_game_category(new_game_category_name)
self.assertEqual(
response2.status_code,
status.HTTP_400_BAD_REQUEST)
deftest_retrieve_game_categories_list(self):
"""
Ensurewecanretrieveagamecagory
"""
new_game_category_name='NewGameCategory'
self.create_game_category(new_game_category_name)
url=reverse('gamecategory-list')
response=self.client.get(url,format='json')
self.assertEqual(
response.status_code,
status.HTTP_200_OK)
self.assertEqual(
response.data['count'],
1)
self.assertEqual(
response.data['results'][0]['name'],
new_game_category_name)
deftest_update_game_category(self):
"""
Ensurewecanupdateasinglefieldforagamecategory
"""
new_game_category_name='InitialName'
response=self.create_game_category(new_game_category_name)
url=reverse(
'gamecategory-detail',
None,
{response.data['pk']})
updated_game_category_name='UpdatedGameCategoryName'
data={'name':updated_game_category_name}
patch_response=self.client.patch(url,data,format='json')
self.assertEqual(
patch_response.status_code,
status.HTTP_200_OK)
self.assertEqual(
patch_response.data['name'],
updated_game_category_name)
deftest_filter_game_category_by_name(self):
"""
Ensurewecanfilteragamecategorybyname
"""
game_category_name1='Firstgamecategoryname'
self.create_game_category(game_category_name1)
game_caregory_name2='Secondgamecategoryname'
self.create_game_category(game_caregory_name2)
filter_by_name={'name':game_category_name1}
url='{0}?{1}'.format(
reverse('gamecategory-list'),
urlencode(filter_by_name))
response=self.client.get(url,format='json')
self.assertEqual(
response.status_code,
status.HTTP_200_OK)
self.assertEqual(
response.data['count'],
1)
self.assertEqual(
response.data['results'][0]['name'],
game_category_name1)
Weaddedthefollowingmethodsthatstartwhosenamestartwiththetest_prefix:
test_create_duplicated_game_category:Testswhethertheuniqueconstraintsdon'tmakeitpossibleforustocreatetwogamecategorieswiththesamename.ThesecondtimewecomposeandsendanHTTPPOSTrequestwithaduplicatecategoryname,wemustreceiveanHTTP400BadRequeststatuscode(status.HTTP_400_BAD_REQUEST)test_retrieve_game_categories_list:Testswhetherwecanretrieveaspecificgamecategorybyitsprimarykeyoridtest_update_game_category:Testswhetherwecanupdateasinglefieldforagamecategorytest_filter_game_category_by_name:Testswhetherwecanfilteragamecategorybyname
Tip
Notethateachtestthatrequiresaspecificconditioninthedatabasemustexecuteallthenecessarycodeforthedatabasetobeinthisspecificcondition.Forexample,inordertoupdateanexistinggamecategory,firstwemustcreateanewgamecategoryandthenwecanupdateit.Eachtestmethodwillbeexecutedwithoutdatafromthepreviouslyexecutedtestmethodsinthedatabase,thatis,eachtestwillrunwithadatabasecleanedofdatafromprevioustests.
ThelastthreemethodsintheprecedinglistcheckthedataincludedintheresponseJSON
bodybyinspectingthedataattributefortheresponse.Forexample,thefirstlinecheckswhetherthevalueforcountisequalto1andthenextlinescheckwhetherthenamekeyforthefirstelementintheresultsarrayisequaltothevalueholdinthenew_game_category_namevariable:
self.assertEqual(response.data['count'],1)
self.assertEqual(
response.data['results'][0]['name'],
new_game_category_name)
Thetest_filter_game_category_by_namemethodcallsthedjango.utils.http.urlencodefunctiontogenerateanencodedURLfromthefilter_by_namedictionarythatspecifiesthefieldnameandthevaluewewanttousetofiltertheretrieveddata.ThefollowinglinesshowthecodethatgeneratestheURLandsavesitintheurlvariable.Ifgame_cagory_name1is'Firstgamecategoryname',theresultofthecalltotheurlencodefunctionwillbe'name=First+game+category+name'.
filter_by_name={'name':game_category_name1}
url='{0}?{1}'.format(
reverse('gamecategory-list'),
urlencode(filter_by_name))
RunningunittestsandcheckingtestingcoverageNow,runthefollowingcommandtocreateatestdatabase,runallthemigrationsandusetheDjangonosetestrunningtoexecuteallthetestswecreated.ThetestrunnerwillexecuteallthemethodsforourGameCategoryTestsclassthatstartwiththetest_prefixandwilldisplaytheresults.
Tip
Thetestswon'tmakechangestothedatabasewehavebeenusingwhenworkingontheAPI.
Rememberthatweconfiguredmanydefaultcommand-lineoptionsthatwillbeusedwithouttheneedtoentertheminourcommand-line.Runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing.Wewillusethe-v2optiontousetheverbositylevel2becausewewanttocheckallthethingsthatthetestrunnerisdoing:
pythonmanage.pytest-v2
Thefollowinglinesshowthesampleoutput:
nosetests--with-coverage--cover-package=games--cover-erase--cover-
inclusive-v--verbosity=2
Creatingtestdatabaseforalias'default'('test_games')...
Operationstoperform:
Synchronizeunmigratedapps:django_nose,staticfiles,crispy_forms,
messages,rest_framework
Applyallmigrations:games,admin,auth,contenttypes,sessions
Synchronizingappswithoutmigrations:
Creatingtables...
RunningdeferredSQL...
Runningmigrations:
Renderingmodelstates...DONE
Applyingcontenttypes.0001_initial...OK
Applyingauth.0001_initial...OK
Applyingadmin.0001_initial...OK
Applyingadmin.0002_logentry_remove_auto_add...OK
Applyingcontenttypes.0002_remove_content_type_name...OK
Applyingauth.0002_alter_permission_name_max_length...OK
Applyingauth.0003_alter_user_email_max_length...OK
Applyingauth.0004_alter_user_username_opts...OK
Applyingauth.0005_alter_user_last_login_null...OK
Applyingauth.0006_require_contenttypes_0002...OK
Applyingauth.0007_alter_validators_add_error_messages...OK
Applyinggames.0001_initial...OK
Applyinggames.0002_auto_20160623_2131...OK
Applyinggames.0003_game_owner...OK
Applyingsessions.0001_initial...OK
EnsurewecancreateanewGameCategoryandthenretrieveit...ok
EnsurewecancreateanewGameCategory....ok
Ensurewecanfilteragamecategorybyname...ok
Ensurewecanretrieveagamecagory...ok
Ensurewecanupdateasinglefieldforagamecategory...ok
NameStmtsMissCover
------------------------------------------
games.py00100%
games/admin.py110%
games/apps.py330%
games/models.py36353%
games/pagination.py30100%
games/permissions.py6350%
games/serializers.py450100%
games/urls.py30100%
games/views.py91298%
------------------------------------------
TOTAL1884477%
------------------------------------------
Ran5testsin0.143s
OK
Destroyingtestdatabaseforalias'default'('test_games')...
Theoutputprovidesthedetailsindicatingthatthetestrunnerexecuted5testsandallofthempassed.Afterthedetailsaboutthemigrationsareexecuted,theoutputdisplaysthecommentsweincludedforeachmethodintheGameCategoryTestsclassthatstartedwiththetest_prefixandrepresentedatesttobeexecuted.Thefollowinglistshowsthedescriptionincludedinthecommentsandthemethodthattheyrepresent:
EnsureswecancreateanewGameCategoryandthenretrieveit:test_create_and_retrieve_game_category.EnsureswecancreateanewGameCategory:test_create_duplicated_game_category.Ensureswecanfilteragamecategorybyname:test_retrieve_game_categories_list.Ensureswecanretrieveagamecagory:test_update_game_category.Ensureswecanupdateasinglefieldforagamecategory:test_filter_game_category_by_name.
ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageusesthecodeanalysistoolsandthetracinghooksincludedinthePythonstandardlibrarytodeterminewhichlinesofcodeareexecutableandwhichoftheselineshavebeenexecuted.Thereportprovidesatablewiththefollowingcolumns:
Name:ThePythonmodulename.Stmts:ThecountofexecutablestatementsforthePythonmodule.Miss:Thenumberofexecutablestatementsmissed,thatis,theonesthatweren'texecuted.Cover:Thecoverageofexecutablestatements,expressedasapercentage.
Wedefinitelyhaveaverylowcoverageformodels.pybasedonthemeasurementsshowninthereport.Infact,wejustwroteafewtestsrelatedtotheGameCategorymodel,andtherefore,itmakessensethatthecoverageisreallylowforthemodels:
Wecanrunthecoveragecommandwiththe-mcommand-lineoptiontodisplaytheline
numbersofthemissingstatementsinanewMissingcolumn.
coveragereport-m
Thecommandwillusetheinformationfromthelastexecutionandwilldisplaythemissingstatements.Thenextlinesshowasampleoutputthatcorrespondtothepreviousexecutionoftheunittests:
NameStmtsMissCoverMissing
----------------------------------------------------
games/__init__.py00100%
games/admin.py110%1
games/apps.py330%1-5
games/models.py36353%1-10,14-70
games/pagination.py30100%
games/permissions.py6350%6-9
games/serializers.py450100%
games/tests.py550100%
games/urls.py30100%
games/views.py91298%83,177
----------------------------------------------------
TOTAL2434482%
Now,runthefollowingcommandtogetannotatedHTMLlistingsdetailingmissedlines:
coveragehtml
Opentheindex.htmlHTMLfilegeneratedinthehtmlcovfolderwithyourwebbrowser.ThefollowingpictureshowsanexamplereportthatcoveragegeneratedinHTMLformat.
Clickortapongames/models.pyandthewebbrowserwillrenderawebpagethatdisplaysthestatementsthatwererun,themissingonesandtheexcluded,withdifferentcolors.Wecanclickortapontherun,missing,andexcludedbuttonstoshoworhidethebackgroundcolorthatrepresentsthestatusforeachlineofcode.Bydefault,themissinglinesofcodewillbedisplayedwithapinkbackground.Thus,wemustwriteunitteststhattargettheselinesofcodetoimproveourtestscoverage:
ImprovingtestingcoverageNow,wewillwriteadditionalunitteststoimprovethetestingcoverage.Specifically,wewillwriteunittestsrelatedtotheplayerclassbasedviews:PlayerListandPlayerDetail.Opentheexistinggames/test.pyfileandinsertthefollowinglinesafterthelastlinethatdeclaresimports.WeneedanewimportstatementandwewilldeclarethenewPlayerTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_04_05folder:
fromgames.modelsimportPlayer
classPlayerTests(APITestCase):
defcreate_player(self,name,gender):
url=reverse('player-list')
data={'name':name,'gender':gender}
response=self.client.post(url,data,format='json')
returnresponse
deftest_create_and_retrieve_player(self):
"""
EnsurewecancreateanewPlayerandthenretrieveit
"""
new_player_name='NewPlayer'
new_player_gender=Player.MALE
response=self.create_player(new_player_name,new_player_gender)
self.assertEqual(response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Player.objects.count(),1)
self.assertEqual(
Player.objects.get().name,
new_player_name)
deftest_create_duplicated_player(self):
"""
EnsurewecancreateanewPlayerandwecannotcreateaduplicate.
"""
url=reverse('player-list')
new_player_name='NewFemalePlayer'
new_player_gender=Player.FEMALE
response1=self.create_player(new_player_name,new_player_gender)
self.assertEqual(
response1.status_code,
status.HTTP_201_CREATED)
response2=self.create_player(new_player_name,new_player_gender)
self.assertEqual(
response2.status_code,
status.HTTP_400_BAD_REQUEST)
deftest_retrieve_players_list(self):
"""
Ensurewecanretrieveaplayer
"""
new_player_name='NewFemalePlayer'
new_player_gender=Player.FEMALE
self.create_player(new_player_name,new_player_gender)
url=reverse('player-list')
response=self.client.get(url,format='json')
self.assertEqual(
response.status_code,
status.HTTP_200_OK)
self.assertEqual(
response.data['count'],
1)
self.assertEqual(
response.data['results'][0]['name'],
new_player_name)
self.assertEqual(
response.data['results'][0]['gender'],
new_player_gender)
ThePlayerTestsclassisasubclassofrest_framework.test.APITestCase.Theclassdeclaresthecreate_playermethodthatreceivesthedesirednameandgenderforthenewplayerasarguments.ThemethodbuildstheurlandthedatadictionarytocomposeandsendanHTTPPOSTmethodtotheviewassociatedwiththeplayer-listviewnameandreturnstheresponsegeneratedbythisrequest.Manytestmethodswillcallthecreate_playermethodtocreateaplayerandthencomposeandsendotherHTTPrequeststotheAPI.
Theclassdeclaresthefollowingmethodsthatstartwhosenamestartwiththetest_prefix:
test_create_and_retrieve_player:TestswhetherwecancreateanewPlayerandthenretrieveit.test_create_duplicated_player:Testswhethertheuniqueconstraintsdon'tmakeitpossibleforustocreatetwoplayerswiththesamename.ThesecondtimewecomposeandsendanHTTPPOSTrequestwithaduplicateplayername,wemustreceiveanHTTP400BadRequeststatuscode(status.HTTP_400_BAD_REQUEST).test_retrieve_player_list:Testswhetherwecanretrieveaspecificgamecategorybyitsprimarykeyorid.
Wejustcodedafewtestsrelatedtoplayerstoimprovetestcoverageandnoticetheimpactonthetestcoveragereport.
Now,runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing.Wewillusethe-v2optiontousetheverbositylevel2becausewewanttocheckallthethingsthatthetestrunnerisdoing:
pythonmanage.pytest-v2
Thefollowinglinesshowthelastlinesofthesampleoutput:
EnsurewecancreateanewGameCategoryandthenretrieveit...ok
EnsurewecancreateanewGameCategory....ok
Ensurewecanfilteragamecategorybyname...ok
Ensurewecanretrieveagamecagory...ok
Ensurewecanupdateasinglefieldforagamecategory...ok
EnsurewecancreateanewPlayerandthenretrieveit...ok
EnsurewecancreateanewPlayerandwecannotcreateaduplicate....ok
Ensurewecanretrieveaplayer...ok
NameStmtsMissCover
------------------------------------------
games.py00100%
games/admin.py110%
games/apps.py330%
games/models.py36346%
games/pagination.py30100%
games/permissions.py6350%
games/serializers.py450100%
games/urls.py30100%
games/views.py91298%
------------------------------------------
TOTAL1884377%
----------------------------------------------------------------------
Ran8testsin0.168s
OK
Destroyingtestdatabaseforalias'default'('test_games')...
Theoutputprovidesdetailsthatindicatethatthetestrunnerexecuted8testsandallofthempassed.ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageincreasedtheCoverpercentagefrom3%inthepreviousrunto6%.TheadditionaltestswewroteexecutecodeforthePlayermodel,andtherefore,thereisanimpactinthecoveragereport.
Tip
Wejustcreatedafewunitteststounderstandhowwecancodethem.However,ofcourse,itwouldbenecessarytowritemoreteststoprovideanappropriatecoverageofallthefeaturedandexecutionscenariosincludedintheAPI.
UnderstandingstrategiesfordeploymentsandscalabilityOneofthebiggestdrawbacksrelatedtoDjangoandDjangoRESTFrameworkisthateachHTTPrequestisblocking.Thus,whenevertheDjangoserverreceivesanHTTPrequest,itdoesn'tstartworkingonanyotherHTTPrequestsintheincomingqueueuntiltheserversendstheresponseforthefirstHTTPrequestitreceived.
However,oneofthegreatestadvantagesofRESTfulWebServicesisthattheyarestateless,thatis,theyshouldn'tkeepaclientstateonanyserver.OurAPIisagoodexampleofastatelessRESTfulWebService.Thus,wecanmaketheAPIrunonasmanyserversasnecessarytoachieveourscalabilitygoals.Obviously,wemusttakeintoaccountthatwecaneasilytransformthedatabaseserverinourscalabilitybottleneck.
Tip
Nowadays,wehaveahugenumberofcloud-basedalternativestodeployaRESTfulwebservicethatusesDjangoandDjangoRESTFrameworkandmakeitextremelyscalable.Justtomentionafewexamples,wehaveHeroku,PythonAnywhere,GoogleAppEngine,OpenShift,AWSElasticBeanstalk,andWindowsAzure.
Eachplatformincludesdetailedinstructionstodeployourapplication.Allofthemwillrequireustogeneratetherequirements.txtfilethatliststheapplicationdependenciestogetherwiththeirversions.Thisway,theplatformswillbeabletoinstallallthenecessarydependencieslistedinthefile.
Runthefollowingpipfreeze,togeneratetherequirements.txtfile:
pipfreeze>requirements.txt
Thefollowinglinesshowthecontentsofasamplegeneratedrequirements.txtfile.However,bearinmindthatmanypackagesincreasetheirversionnumberquicklyandyoumightseedifferentversionsinyourconfiguration:
coverage==4.1
Django==1.9.7
django-braces==1.9.0
django-crispy-forms==1.6.0
django-filter==0.13.0
django-nose==1.4.4
django-oauth-toolkit==0.10.0
djangorestframework==3.3.3
nose==1.3.7
oauthlib==1.0.3
psycopg2==2.6.2
six==1.10.0
WealwayshavetomakesurethatweprofiletheAPIandthedatabasebeforewedeployourfirstversionoftheRESTfulWebService.Itisveryimportanttomakesurethatthegeneratedqueriesrunproperlyontheunderlyingdatabaseandthatthemostpopularqueriesdonotendupinsequentialscans.Itisusuallynecessarytoaddtheappropriateindexestothetablesinthedatabase.
WehavebeenusingbasicHTTPauthentication.Incasewedecidetousethisauthenticationorothermechanisms,wemustmakesurethattheAPIrunsunderHTTPSinproductionenvironments.Inaddition,wemustmakesurethatwechangethefollowinglineinthesettings.pyfile:
DEBUG=True
Wemustalwaysturnoffthedebugmodeinproduction,andtherefore,wemustreplacethepreviouslinewiththefollowingone:
DEBUG=False
Testyourknowledge1. TheScopedRateThrottleclass:
1. Limitstherateofrequeststhataspecificusercanmake.2. LimitstherateofrequestsforspecificpartsoftheAPIidentifiedwiththevalue
assignedtothethrottle_scopeproperty.3. Limitstherateofrequeststhatananonymoususercanmake.
2. TheUserRateThrottleclass:1. Limitstherateofrequeststhataspecificusercanmake.2. LimitstherateofrequestsforspecificpartsoftheAPIidentifiedwiththevalue
assignedtothethrottle_scopeproperty.3. Limitstherateofrequeststhatananonymoususercanmake.
3. TheDjangoFilterBackendclass:1. Providessinglequeryparameterbasedsearchingcapabilitiesanditisbasedonthe
Djangoadmin'ssearchfunction.2. Allowstheclienttocontrolhowtheresultsareorderedwithasinglequery
parameter.3. Providesfieldfilteringcapabilities.
4. TheSearchFilterclass:1. Providessinglequeryparameterbasedsearchingcapabilitiesanditisbasedonthe
Djangoadmin'ssearchfunction.2. Allowstheclienttocontrolhowtheresultsareorderedwithasinglequery
parameter.3. Providesfieldfilteringcapabilities.
5. InasubclassofAPITestCase,self.clientis:1. TheAPIClientinstancethatallowsustoeasilycomposeandsendHTTPrequests
fortesting.2. TheAPITestClientinstancethatallowsustoeasilycomposeandsendHTTP
requestsfortesting.3. TheAPITestCaseinstancethatallowsustoeasilycomposeandsendHTTPrequests
fortesting.
SummaryInthischapter,wetookadvantageofthefeaturesincludedinDjangoRESTFrameworktodefinethrottlingpolicies.Weusedfiltering,searching,andorderingclassestomakeiteasytoconfigurefilters,searchqueries,anddesiredorderfortheresultsinHTTPrequests.WeusedthebrowsableAPIfeaturetotestthesenewfeaturesincludedinourAPI.
Wewrotethefirstroundofunittests,measuredtestcoverage,andthenwewroteadditionalunitteststoimprovetestcoverage.Finally,weunderstoodmanyconsiderationsfordeploymentandscalability.
NowthatwebuiltacomplexAPIwithDjangoRESTFrameworkandtestedit,wewillmovetoanotherpopularPythonwebframework,Flask,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter5.DevelopingRESTfulAPIswithFlaskInthischapter,wewillstartworkingwithFlaskanditsFlask-RESTfulextension;wewillalsocreateaRESTfulWebAPIthatperformsCRUDoperationsonasimplelist.Wewill:
DesignaRESTfulAPIthatperformsCRUDoperationsinFlaskwiththeFlask-RESTfulextensionUnderstandthetasksperformedbyeachHTTPmethodSetupthevirtualenvironmentwithFlaskanditsFlask-RESTfulextensionDeclarestatuscodesfortheresponsesCreatethemodeltorepresentaresourceUseadictionaryasarepositoryConfigureoutputfieldsforserializedresponsesWorkwithresourcefulroutingontopofFlaskpluggableviewsConfigureresourceroutingandendpointsMakeHTTPrequeststotheFlaskAPIWorkwithcommand-linetoolstointeractwiththeFlaskAPIWorkwithGUItoolstointeractwiththeFlaskAPI
DesigningaRESTfulAPItointeractwithasimpledatasourceImaginethatwehavetoconfigurethemessagestobedisplayedinanOLEDdisplaywiredtoanIoT (InternetofThings)device,theIoTdeviceiscapableofrunningPython3.5,Flask,andotherPythonpackages.ThereisateamthatiswritingcodethatretrievesstringmessagesfromadictionaryanddisplaysthemintheOLEDdisplaywiredtotheIoTdevice.WehavetostartworkingonamobileappandawebsitethathastointeractwithaRESTfulAPItoperformCRUDoperationswithstringmessages.
Wedon'tneedanORMbecausewewon'tpersistthestringmessagesonadatabase.Wewilljustworkwithanin-memorydictionaryasourdatasource.ItisoneoftherequirementsforthisRESTfulAPI.Inthiscase,theRESTfulwebservicewillberunningontheIoTdevice,thatis,wewillruntheFlaskdevelopmentserverontheIoTdevice.
Tip
WewilldefinitelylosescalabilityforourRESTfulAPIbecausewehavethein-memorydatasourceintheserver,andtherefore,wecannotruntheRESTfulAPIinanotherIoTdevice.However,wewillworkwithanotherexamplerelatedtoamorecomplexdatasourcethatwillbeabletoscaleintheRESTfulwaylater.ThefirstexampleisgoingtoallowustounderstandhowFlaskandFlask-RESTfulworktogetherwithaverysimplein-memorydatasource.
WehavechosenFlaskbecauseitismorelightweightthanDjango,wedon'tneedtoconfigureanORMandwewanttostartrunningtheRESTfulAPIontheIoTdevice,assoonaspossible,toallowalltheteamstointeractwithit.WewillcodethewebsitewithFlasktoo,andtherefore,wewanttousethesamewebmicro-frameworktopowerthewebsiteandtheRESTfulwebservice.
TherearemanyextensionsavailableforFlaskthatmakesiteasiertoperformspecifictaskswiththeFlaskmicro-framework.WewilltakeadvantageofFlask-RESTful,anextensionthatwillallowustoencouragebestpracticeswhilebuildingourRESTfulAPI.Inthiscase,wewillworkwithaPythondictionaryasthedatasource.Aspreviouslyexplained,wewillworkwithmorecomplexdatasourcesintheforthcomingexamples.
First,wemustspecifytherequirementsforourmainresource:amessage.Weneedthefollowingattributesorfieldsforamessage:
AnintegeridentifierAstringmessageAdurationinsecondsthatindicatesthetimethemessagehastobeprintedontheOLEDdisplayAcreationdateandtime-thetimestampwillbeaddedautomaticallywhenaddinganewmessagetothecollection
Amessagecategorydescription,suchas"Warning"and"Information"AnintegercounterthatindicatesthetimesthemessagehasbeenprintedintheOLEDdisplayAboolvalueindicatingwhetherthemessagewasprintedatleastonceontheOLEDdisplay
ThefollowingtableshowstheHTTPverbs,thescope,andthesemanticsforthemethodsthatourfirstversionoftheAPImustsupport.EachmethodiscomposedbyanHTTPverbandascopeandallthemethodshaveawell-definedmeaningforallthemessagesandcollections.InourAPI,eachmessagehasitsownuniqueURL.
HTTPverb Scope Semantics
GETCollectionofmessages
Retrieveallthestoredmessagesinthecollection,sortedbytheirnameinascendingorder
GET Message Retrieveasinglemessage
POSTCollectionofmessages Createanewmessageinthecollection
PATCH Message Updateafieldforanexistingmessage
DELETE Message Deleteanexistingmessage
UnderstandingthetasksperformedbyeachHTTPmethodLet'sconsiderthathttp://localhost:5000/api/messages/istheURLforthecollectionofmessages.IfweaddanumbertotheprecedingURL,weidentifyaspecificmessagewhoseidisequaltothespecifiednumericvalue.Forexample,http://localhost:5000/api/messsages/6identifiesthemessagewhoseidisequalto6.
Tip
WewantourAPItobeabletodifferentiatecollectionsfromasingleresourceofthecollectionintheURLs.Whenwereferacollection,wewilluseaslash(/)asthelastcharacterfortheURL,asinhttp://localhost:5000/api/messages/.Whenwerefertoasingleresourceofthecollectionwewon'tuseaslash(/)asthelastcharacterfortheURL,asinhttp://localhost:5000/api/messages/6.
WehavetocomposeandsendanHTTPrequestwiththePOSTHTTPverbandthehttp://localhost:5000/api/messages/requestURLtocreateanewmessage.Inaddition,wehavetoprovidetheJSONkey-valuepairswiththefieldnamesandthevaluestocreatethenewmessage.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefields,makesurethatitisavalidmessage,andpersistitinthemessagesdictionary.
Theserverwillreturna201CreatedstatuscodeandaJSONbodywiththerecentlyaddedmessageserializedtoJSON,includingtheassignedidthatwasautomaticallygeneratedbytheservertothemessageobject:
POSThttp://localhost:5000/api/messages/
WehavetocomposeandsendanHTTPrequestwiththeGETHTTPverbandthehttp://localhost:5000/api/messages/{id}requestURLtoretrievethemessagewhoseidmatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Forexample,ifweusetherequestURLhttp://localhost:5000/api/messages/82,theserverwillretrievethegamewhoseidmatches82.Asaresultoftherequest,theserverwillretrieveamessagewiththespecifiedidfromthedictionary.
Ifamessageisfound,theserverwillserializethemessageobjectintoJSONandreturna200OKstatuscodeandaJSONbodywiththeserializedmessageobject.Ifnomessagematchesthespecifiedidorprimarykey,theserverwillreturna404NotFoundstatus:
GEThttp://localhost:5000/api/messages/{id}
WehavetocomposeandsendanHTTPrequestwiththePATCHHTTPverbandthehttp://localhost:5000/api/messages/{id}requestURLtoupdateoneormorefieldsforthemessagewhoseidmatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Inaddition,wehavetoprovidetheJSONkey-valuepairswiththefieldnamestobeupdated
andtheirnewvalues.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefields,updatethesefieldsonthemessagethatmatchesthespecifiedid,andupdatethemessageinthedictionary,ifitisavalidmessage.
Theserverwillreturna200OKstatuscodeandaJSONbodywiththerecentlyupdatedgameserializedtoJSON.Ifweprovideinvaliddataforthefieldstobeupdated,theserverwillreturna400BadRequeststatuscode.Iftheserverdoesn'tfindamessagewiththespecifiedid,theserverwillreturnjusta404NotFoundstatus:
PATCHhttp://localhost:5000/api/messages/{id}
Tip
ThePATCHmethodwillallowustoeasilyupdatetwofieldsforamessage:theintegercounter,thatindicatesthetimesthemessagehasbeenprintedandtheboolvalue,thatspecifieswhetherthemessagewasprintedatleastonce.
WehavetocomposeandsendanHTTPrequestwiththeDELETEHTTPverbandthehttp://localhost:5000/api/messages/{id}requestURLtoremovethemessagewhoseidmatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Forexample,ifweusetherequestURLhttp://localhost:5000/api/messages/15,theserverwilldeletethemessagewhoseidmatches15.Asaresultoftherequest,theserverwillretrieveamessagewiththespecifiedidfromthedictionary.Ifamessageisfound,theserverwillrequestthedictionarytodeletetheentryassociatedwiththismessageobjectandreturna204NoContentstatuscode.Ifnomessagematchesthespecifiedid,theserverwillreturna404NotFoundstatus:
DELETEhttp://localhost:5000/api/messages/{id}
SettingupavirtualenvironmentwithFlaskandFlask-RESTfulInChapter1,DevelopingRESTfulAPIswithDjango,welearnedthat,throughoutthisbook,weweregoingtoworkwiththelightweightvirtualenvironmentsintroducedinPython3.4andimprovedinPython3.4.Now,wewillfollowthestepstocreateanewlightweightvirtualenvironmenttoworkwithFlaskandFlask-RESTful.ItishighlyrecommendedtoreadChapter1,DevelopingRESTfulAPIswithDjango,incaseyoudon'thaveexperiencewithlightweightvirtualenvironmentsinPython.Thechapterincludesallthedetailedexplanationsoftheeffectsofthestepswearegoingtofollow.
First,wehavetoselectthetargetfolderordirectoryforourvirtualenvironment.WewillusethefollowingpathintheexampleformacOSandLinux.ThetargetfolderforthevirtualenvironmentwillbethePythonREST/Flask01folderwithinourhomedirectory.Forexample,ifourhomedirectoryinmacOSorLinuxis/Users/gaston,thevirtualenvironmentwillbecreatedwithin/Users/gaston/PythonREST/Flask01.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand,asshown:
~/PythonREST/Flask01
WewillusethefollowingpathintheexampleforWindows.ThetargetfolderforthevirtualenvironmentwillbethePythonREST\Flask01folderwithinouruserprofilefolder.Forexample,ifouruserprofilefolderisC:\Users\Gaston,thevirtualenvironmentwillbecreatedwithinC:\Users\gaston\PythonREST\Flask01.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand,asshown:
%USERPROFILE%\PythonREST\Flask01
OpenaTerminalinmacOSorLinuxandexecutethefollowingcommandtocreateavirtualenvironment:
python3-mvenv~/PythonREST/Flask01
InWindows,executethefollowingcommandtocreateavirtualenvironment:
python-mvenv%USERPROFILE%\PythonREST\Flask01
Theprecedingcommanddoesn'tproduceanyoutput.Nowthatwehavecreatedavirtualenvironment,wewillrunaplatform-specificscripttoactivateit.Afterweactivatethevirtualenvironment,wewillinstallpackagesthatwillonlybeavailableinthisvirtualenvironment.
IfyourTerminalisconfiguredtousethebashshellinmacOSorLinux,runthefollowingcommandtoactivatethevirtualenvironment.Thecommandalsoworksforthezshshell:
source~/PythonREST/Flask01/bin/activate
IfyourTerminalisconfiguredtouseeitherthecshortcshshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Flask01/bin/activate.csh
IfyourTerminalisconfiguredtouseeitherthefishshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Flask01/bin/activate.fish
InWindows,youcanruneitherabatchfileintheCommandPromptoraWindowsPowerShellscripttoactivatethevirtualenvironment.IfyouprefertheCommandPrompt,runthefollowingcommandintheWindowscommandlinetoactivatethevirtualenvironment:
%USERPROFILE%\PythonREST\Flask01\Scripts\activate.bat
IfyouprefertheWindowsPowerShell,launchitandrunthefollowingcommandstoactivatethevirtualenvironment.However,notethatyoushouldhavethescriptsexecutionenabledinWindowsPowerShelltobeabletorunthescript:
cd$env:USERPROFILE
PythonREST\Flask01\Scripts\Activate.ps1
Afteryouactivatethevirtualenvironment,theCommandPromptwilldisplaythevirtualenvironmentrootfoldername,enclosedinparenthesis,asaprefixforthedefaultprompt,toremindusthatweareworkinginthevirtualenvironment.Inthiscase,wewillsee(Flask01)asaprefixfortheCommandPromptbecausetherootfolderfortheactivatedvirtualenvironmentisFlask01.
Wehavecreatedandactivatedavirtualenvironment.NowitistimetorunthecommandsthatwillbethesameformacOS,Linux,orWindows;wemustrunthefollowingcommandtoinstallFlask-RESTfulwithpip.FlaskisadependencyforFlask-RESTful,andtherefore,pipwillinstallitautomatically,too:
pipinstallflask-restful
Thelastlinesfortheoutputwillindicateallthepackagesthathavebeensuccessfullyinstalled,includingflask-restfulandFlask:
Installingcollectedpackages:six,pytz,click,itsdangerous,MarkupSafe,
Jinja2,Werkzeug,Flask,python-dateutil,aniso8601,flask-restful
Runningsetup.pyinstallforclick
Runningsetup.pyinstallforitsdangerous
Runningsetup.pyinstallforMarkupSafe
Runningsetup.pyinstallforaniso8601
SuccessfullyinstalledFlask-0.11.1Jinja2-2.8MarkupSafe-0.23Werkzeug-
0.11.10aniso8601-1.1.0click-6.6flask-restful-0.3.5itsdangerous-0.24
python-dateutil-2.5.3pytz-2016.4six-1.10.0
DeclaringstatuscodesfortheresponsesNeitherFlasknorFlask-RESTfulincludesthedeclarationofvariablesforthedifferentHTTPstatuscodes.Wedon'twanttoreturnnumbersasstatuscodes.Wewantourcodetobeeasytoreadandunderstand,andtherefore,wewillusedescriptiveHTTPstatuscodes.WewillborrowthecodethatdeclaresusefulfunctionsandvariablesrelatedtoHTTPstatuscodesfromthestatus.pyfileincludedinDjangoRESTFramework,thatis,theframeworkwehavebeenusingintheprecedingchapters.
First,createafoldernamedapiwithintherootfolderfortherecentlycreatedvirtualenvironment,andthencreateanewstatus.pyfilewithintheapifolder.ThefollowinglinesshowthecodethatdeclaresfunctionsandvariableswithdescriptiveHTTPstatuscodesintheapi/models.pyfileborrowedfromtherest_framework.statusmodule.Wedon'twanttoreinventthewheel,andthemoduleprovideseverythingweneedtoworkwithHTTPstatuscodesinourFlask-basedAPI.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder:
defis_informational(code):
returncode>=100andcode<=199
defis_success(code):
returncode>=200andcode<=299
defis_redirect(code):
returncode>=300andcode<=399
defis_client_error(code):
returncode>=400andcode<=499
defis_server_error(code):
returncode>=500andcode<=599
HTTP_100_CONTINUE=100
HTTP_101_SWITCHING_PROTOCOLS=101
HTTP_200_OK=200
HTTP_201_CREATED=201
HTTP_202_ACCEPTED=202
HTTP_203_NON_AUTHORITATIVE_INFORMATION=203
HTTP_204_NO_CONTENT=204
HTTP_205_RESET_CONTENT=205
HTTP_206_PARTIAL_CONTENT=206
HTTP_300_MULTIPLE_CHOICES=300
HTTP_301_MOVED_PERMANENTLY=301
HTTP_302_FOUND=302
HTTP_303_SEE_OTHER=303
HTTP_304_NOT_MODIFIED=304
HTTP_305_USE_PROXY=305
HTTP_306_RESERVED=306
HTTP_307_TEMPORARY_REDIRECT=307
HTTP_400_BAD_REQUEST=400
HTTP_401_UNAUTHORIZED=401
HTTP_402_PAYMENT_REQUIRED=402
HTTP_403_FORBIDDEN=403
HTTP_404_NOT_FOUND=404
HTTP_405_METHOD_NOT_ALLOWED=405
HTTP_406_NOT_ACCEPTABLE=406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED=407
HTTP_408_REQUEST_TIMEOUT=408
HTTP_409_CONFLICT=409
HTTP_410_GONE=410
HTTP_411_LENGTH_REQUIRED=411
HTTP_412_PRECONDITION_FAILED=412
HTTP_413_REQUEST_ENTITY_TOO_LARGE=413
HTTP_414_REQUEST_URI_TOO_LONG=414
HTTP_415_UNSUPPORTED_MEDIA_TYPE=415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE=416
HTTP_417_EXPECTATION_FAILED=417
HTTP_428_PRECONDITION_REQUIRED=428
HTTP_429_TOO_MANY_REQUESTS=429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE=431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS=451
HTTP_500_INTERNAL_SERVER_ERROR=500
HTTP_501_NOT_IMPLEMENTED=501
HTTP_502_BAD_GATEWAY=502
HTTP_503_SERVICE_UNAVAILABLE=503
HTTP_504_GATEWAY_TIMEOUT=504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED=505
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED=511
ThecodedeclaresfivefunctionsthatreceivetheHTTPstatuscodeinthecodeargumentanddeterminewhichofthefollowingcategoriesthestatuscodebelongsto:informational,success,redirect,clienterror,orservererrorcategories.Wewillusethepreviousvariableswhenwehavetoreturnaspecificstatuscode.Forexample,incasewehavetoreturna404NotFoundstatuscode,wewillreturnstatus.HTTP_404_NOT_FOUND,insteadofjust404.
CreatingthemodelNow,wewillcreateasimpleMessageModelclassthatwewillusetorepresentmessages.Rememberthatwewon'tbepersistingthemodelinthedatabase,andtherefore,inthiscase,ourclasswilljustprovidetherequiredattributesandnomappinginformation.Createanewmodels.pyfileintheapifolder.ThefollowinglinesshowthecodethatcreatesaMessageModelclassintheapi/models.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder:
classMessageModel:
def__init__(self,message,duration,creation_date,message_category):
#Wewillautomaticallygeneratethenewid
self.id=0
self.message=message
self.duration=duration
self.creation_date=creation_date
self.message_category=message_category
self.printed_times=0
self.printed_once=False
TheMessageModelclassjustdeclaresaconstructor,thatis,the__init__method.Thismethodreceivesmanyargumentsandthenusesthemtoinitializetheattributeswiththesamenames:message,duration,creation_date,andmessage_category.Theidattributeissetto0,printed_timesissetto0,andprinted_onceissettoFalse.WewillautomaticallyincrementtheidentifierforeachnewmessagegeneratedwithAPIcalls.
UsingadictionaryasarepositoryNow,wewillcreateaMessageManagerclassthatwewillusetopersisttheMessageModelinstancesinanin-memorydictionary.OurAPImethodswillcallmethodsfortheMessageManagerclasstoretrieve,insert,update,anddeleteMessageModelinstances.Createanewapi.pyfileintheapifolder.ThefollowinglinesshowthecodethatcreatesaMessageManagerclassintheapi/api.pyfile.Inaddition,thefollowinglinesdeclarealltheimportswewillneedforallthecodewewillwriteinthisfile.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder.
fromflaskimportFlask
fromflask_restfulimportabort,Api,fields,marshal_with,reqparse,Resource
fromdatetimeimportdatetime
frommodelsimportMessageModel
importstatus
frompytzimportutc
classMessageManager():
last_id=0
def__init__(self):
self.messages={}
definsert_message(self,message):
self.__class__.last_id+=1
message.id=self.__class__.last_id
self.messages[self.__class__.last_id]=message
defget_message(self,id):
returnself.messages[id]
defdelete_message(self,id):
delself.messages[id]
TheMessageManagerclassdeclaresalast_idclassattributeandinitializesitto0.ThisclassattributestoresthelastidthathasbeengeneratedandassignedtoaMessageModelinstancestoredinadictionary.Theconstructor,thatis,the__init__method,createsandinitializesthemessagesattributeasanemptydictionary.
Thecodedeclaresthefollowingthreemethodsfortheclass:
insert_message:ThismethodreceivesarecentlycreatedMessageModelinstanceinthemessageargument.Thecodeincreasesthevalueforthelast_idclassattributeandthenassignstheresultingvaluetotheidforthereceivedmessage.Thecodeusesself.__class__toreferencethetypeofthecurrentinstance.Finally,thecodeaddsthemessageasavaluetothekeyidentifiedwiththegeneratedid,last_id,intheself.messagesdictionary.get_message:Thismethodreceivestheidofthemessagethathastoberetrievedfromtheself.messagesdictionary.Thecodereturnsthevaluerelatedtothekeythatmatches
thereceivedidintheself.messagesdictionarythatweareusingasourdatasource.delete_message:Thismethodreceivestheidofthemessagethathastoberemovedfromtheself.messagesdictionary.Thecodedeletesthekey-valuepairwhosekeymatchesthereceivedidintheself.messagesdictionarythatweareusingasourdatasource.
Wedon'tneedamethodtoupdateamessagebecausewewilljustmakechangestotheattributesoftheMessageModelinstancethatisalreadystoredintheself.messagesdictionary.ThevaluestoredinthedictionaryisareferencetotheMessageModelinstancethatweareupdating,andtherefore,wedon'tneedtocallaspecificmethodtoupdatetheinstanceinthedictionary.However,incasewewereworkingwithadatabase,wewouldneedtocallanupdatemethodforourORMordatarepository.
ConfiguringoutputfieldsNow,wewillcreateamessage_fieldsdictionarythatwewillusetocontrolthedatathatwewantFlask-RESTfultorenderinourresponse,whenwereturnMessageModelinstances.Openthepreviouslycreatedapi/api.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder.
message_fields={
'id':fields.Integer,
'uri':fields.Url('message_endpoint'),
'message':fields.String,
'duration':fields.Integer,
'creation_date':fields.DateTime,
'message_category':fields.String,
'printed_times':fields.Integer,
'printed_once':fields.Boolean
}
message_manager=MessageManager()
Wedeclaredthemessage_fieldsdictionary(dict)withkey-valuepairsofstringsandclassesdeclaredintheflask_restful.fieldsmodule.ThekeysarethenamesoftheattributeswewanttorenderfromtheMessageModelclassandthevaluesaretheclassesthatformatandreturnthevalueforthefield.Inthepreviouscode,weworkedwiththefollowingclasses,thatformatandreturnthevalueforthespecifiedfieldinthekey:
field.Integer:Outputsanintegervalue.fields.Url:GeneratesastringrepresentationofaURL.Bydefault,thisclassgeneratesarelativeURIfortheresourcethatisbeingrequested.Thecodespecifies'message_endpoint'fortheendpointargument.Thisway,theclasswillusethespecifiedendpointname.Wewilldeclarethisendpointlaterintheapi.pyfile.Wedon'twanttoincludethehostnameinthegeneratedURI,andtherefore,weusethedefaultvaluefortheabsoluteboolattribute,whichisFalse.fields.DateTime:OutputsaformatteddatetimestringinUTC,inthedefaultRFC822format.fields.Boolean:Generatesastringrepresentationofaboolvalue.
The'uri'fieldusesfields.UrlanditisrelatedtothespecifiedendpointinsteadofbeingassociatedtoanattributeoftheMessageModelclass.Itistheonlycaseinwhichthespecifiedfieldnamedoesn'thaveanattributeintheMessageModelclass.Theotherstringsspecifiedaskeysindicatealltheattributeswewanttoberenderedintheoutputwhenweusethemessage_fieldsdictionarytomakeupthefinalserializedresponseoutput.
Afterwedeclaredthemessage_fieldsdictionary,thenextlineofcodecreatesaninstanceofthepreviouslycreatedMessageManagerclassnamedmessage_manager.Wewillusethisinstancetocreate,retrieve,anddeleteMessageModelinstances.
WorkingwithresourcefulroutingontopofFlaskpluggableviewsFlask-RESTfulusesresourcesbuiltontopofFlaskpluggableviewsasthemainbuildingblockforaRESTfulAPI.Wejustneedtocreateasubclassoftheflask_restful.ResourceclassanddeclarethemethodsforeachsupportedHTTPverb.Asubclassofflask_restful.ResourcerepresentsaRESTfulresourceandtherefore,wewillhavetodeclareoneclasstorepresentthecollectionofmessagesandanotheronetorepresentthemessageresource.
First,wewillcreateaMessageclassthatwewillusetorepresentthemessageresource.Openthepreviouslycreatedapi/api.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder,asshown:
classMessage(Resource):
defabort_if_message_doesnt_exist(self,id):
ifidnotinmessage_manager.messages:
abort(
status.HTTP_404_NOT_FOUND,
message="Message{0}doesn'texist".format(id))
@marshal_with(message_fields)
defget(self,id):
self.abort_if_message_doesnt_exist(id)
returnmessage_manager.get_message(id)
defdelete(self,id):
self.abort_if_message_doesnt_exist(id)
message_manager.delete_message(id)
return'',status.HTTP_204_NO_CONTENT
@marshal_with(message_fields)
defpatch(self,id):
self.abort_if_message_doesnt_exist(id)
message=message_manager.get_message(id)
parser=reqparse.RequestParser()
parser.add_argument('message',type=str)
parser.add_argument('duration',type=int)
parser.add_argument('printed_times',type=int)
parser.add_argument('printed_once',type=bool)
args=parser.parse_args()
if'message'inargs:
message.message=args['message']
if'duration'inargs:
message.duration=args['duration']
if'printed_times'inargs:
message.printed_times=args['printed_times']
if'printed_once'inargs:
message.printed_once=args['printed_once']
returnmessage
TheMessageclassisasubclassofflask_restful.Resourceanddeclaresthefollowingthreemethods,thatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:Thismethodreceivestheidofthemessagethathastoberetrievedintheidargument.Thecodecallstheself.abort_if_message_doesnt_existmethodtoabortincasethereisnomessagewiththerequestedid.Incasethemessageexists,thecodereturnstheMessageModelinstancewhoseidthatmatchesthespecifiedidreturnedbythemessage_manager.get_messagemethod.Thegetmethodusesthe@marshal_withdecoratorwithmessage_fieldsasanargument.ThedecoratorwilltaketheMessageModelinstanceandapplythefieldfilteringandoutputformattingspecifiedinmessage_fields.delete:Thismethodreceivestheidofthemessagethathastobedeletedintheidargument.Thecodecallstheself.abort_if_message_doesnt_existmethodtoabort,incasethereisnomessagewiththerequestedid.Incasethe```messageexists,thecodecallsthemessage_manager.delete_messagemethodwiththereceivedidasanargumenttoremovetheMessageModelinstancefromourdatarepository.Then,thecodereturnsanemptyresponsebodyanda204NoContentstatuscode.patch:Thismethodreceivestheidofthemessagethathastobeupdatedorpatchedintheidargument.Thecodecallstheself.abort_if_message_doesnt_existmethodtoabortincasethereisnomessagewiththerequestedid.Incasethemessageexists,thecodesavestheMessageModelinstancewhoseidthatmatchesthespecifiedidreturnedbythemessage_manager.get_messagemethodinthemessagevariable.Thenextlinecreatesaflask_restful.reqparse.RequestParserinstancenamedparser.TheRequestParserinstanceallowsustoaddargumentswiththeirnamesandtypesandtheneasilyparsetheargumentsreceivedwiththerequest.Thecodemakesfourcallstotheparser.add_argumentwiththeargumentnameandthetypeofthefourargumentswewanttoparse.Then,thecodecallstheparser.parse_argsmethodtoparsealltheargumentsfromtherequestandsavesthereturneddictionary(dict)intheargsvariable.ThecodeupdatesalltheattributesthathavenewvaluesintheargsdictionaryintheMessageModelinstance:message.Incasetherequestdidn'tincludevaluesforcertainfields,thecodewon'tmakechangestotherealtedattributes.Therequestdoesn'trequiretoincludethefourfieldsthatcanbeupdatedwithvalues.Thecodereturnstheupdatedmessage.Thepatchmethodusesthe@marshal_withdecoratorwithmessage_fieldsasanargument.ThedecoratorwilltaketheMessageModelinstance,message,andapplythefieldfilteringandoutputformattingspecifiedinmessage_fields.
Tip
Weusedmultiplereturnvaluestosettheresponsecode.
Aspreviouslyexplained,thethreemethodscalltheinternalabort_if_message_doesnt_existmethodthatreceivestheidforanexistingMessageModelinstanceintheidargument.Ifthereceivedidisnotpresentinthekeysofthemessage_manager.messagesdictionary,themethodcallstheflask_restful.abortfunctionwithstatus.HTTP_404_NOT_FOUNDasthehttp_status_codeargumentandamessageindicatingthatthemessagewiththespecifiedid
doesn'texists.TheabortfunctionraisesanHTTPExceptionforthereceivedhttp_status_codeandattachestheadditionalkeywordargumentstotheexceptionforlaterprocessing.Inthiscase,wegenerateanHTTP404NotFoundstatuscode.
Boththegetandpatchmethodsusethe@marshal_withdecoratorthattakesasingledataobjectoralistofdataobjectsandappliesthefieldfilteringandoutputformattingspecifiesasanargument.Themarshallingcanalsoworkwithdictionaries(dicts).Inbothmethods,wespecifiedmessage_fieldsasanargument,andtherefore,thecoderendersthefollowingfields:id,uri,message,duration,creation_date,message_category,printed_timesandprinted_once.Whenweusethe@marshal_withdecorator,weareautomaticallyreturninganHTTP200OKstatuscode.
Thefollowingreturnstatementwiththe@marshal_with(message_fields)decoratorreturnsanHTTP200OKstatuscodebecausewedidn'tspecifyanystatuscodeafterthereturnedobject(message):
returnmessage
Thenextlineisthelineofcodethatisreallyexecutedwiththe@marshal_with(message_fields)decorator,andwecanuseitinsteadofworkingwiththedecorator:
returnmarshal(message,resource_fields),status.HTTP_200_OK
Forexample,wecancallthemarshalfunctionasshowninthepreviouslineinsteadofusingthe@marshal_withdecoratorandthecodewillproducethesameresult.
Now,wewillcreateaMessageListclassthatwewillusetorepresentthecollectionofmessages.Openthepreviouslycreatedapi/api.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder:
classMessageList(Resource):
@marshal_with(message_fields)
defget(self):
return[vforvinmessage_manager.messages.values()]
@marshal_with(message_fields)
defpost(self):
parser=reqparse.RequestParser()
parser.add_argument('message',type=str,required=True,help='Message
cannotbeblank!')
parser.add_argument('duration',type=int,required=True,help='Duration
cannotbeblank!')
parser.add_argument('message_category',type=str,required=True,
help='Messagecategorycannotbeblank!')
args=parser.parse_args()
message=MessageModel(
message=args['message'],
duration=args['duration'],
creation_date=datetime.now(utc),
message_category=args['message_category']
)
message_manager.insert_message(message)
returnmessage,status.HTTP_201_CREATED
TheMessageListclassisasubclassofflask_restful.ResourceanddeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:ThismethodreturnsalistwithalltheMessageModelinstancessavedinthemessage_manager.messagesdictionary.Thegetmethodusesthe@marshal_withdecoratorwithmessage_fieldsasanargument.ThedecoratorwilltakeeachMessageModelinstanceinthereturnedlistandapplythefieldfilteringandoutputformattingspecifiedinmessage_fields.post:Thismethodcreatesaflask_restful.reqparse.RequestParserinstancenamedparser.TheRequestParserinstanceallowsustoaddargumentswiththeirnamesandtypesandtheneasilyparsetheargumentsreceivedwiththePOSTrequesttocreateanewMessageModelinstance.Thecodemakesthreecallstotheparser.add_argumentwiththeargumentnameandthetypeofthethreeargumentswewanttoparse.Then,thecodecallstheparser.parse_argsmethodtoparsealltheargumentsfromtherequestandsavesthereturneddictionary(dict)intheargsvariable.Thecodeusestheparsedargumentsinthedictionarytospecifythevaluesforthemessage,durationandmessage_categoryattributestocreateanewMessageModelinstanceandsaveitinthemessagevariable.Thevalueforthecreation_dateargumentissettothecurrentdatetimewithtimezoneinfo,andtherefore,itisn'tparsedfromtherequest.Then,thecodecallsthemessage_manager.insert_messagemethodwiththenewMessageModelinstance(message)toaddthisnewinstancetothedictionary.Thepostmethodusesthe@marshal_withdecoratorwithmessage_fieldsasanargument.ThedecoratorwilltaketherecentlycreatedandstoredMessageModelinstance,message,andapplythefieldfilteringandoutputformattingspecifiedinmessage_fields.ThecodereturnsanHTTP201Createdstatuscode.
ThefollowingtableshowsthemethodofourpreviouslycreatedclassesthatwewanttobeexecutedforeachcombinationofHTTPverbandscope:
HTTPverb Scope Classandmethod
GET Collectionofmessages MessageList.get
GET Message Message.get
POST Collectionofmessages MessageList.post
PATCH Message Message.patch
DELETE Message Message.delete
IftherequestresultsintheinvocationofaresourcewithanunsupportedHTTPmethod,Flask-RESTfulwillreturnaresponsewiththeHTTP405MethodNotAllowedstatuscode.
ConfiguringresourceroutingandendpointsWemustmakethenecessaryresourceroutingconfigurationstocalltheappropriatemethodsandpassthemallthenecessaryargumentsbydefiningURLrules.Thefollowinglinescreatethemainentrypointfortheapplication,initializeitwithaFlaskapplicationandconfiguretheresourceroutingfortheapi.Openthepreviouslycreatedapi/api.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_05_01folder:
app=Flask(__name__)
api=Api(app)
api.add_resource(MessageList,'/api/messages/')
api.add_resource(Message,'/api/messages/<int:id>',endpoint='message_endpoint')
if__name__=='__main__':
app.run(debug=True)
Thecodecreatesaninstanceoftheflask_restful.Apiclassandsavesitintheapivariable.Eachcalltotheapi.add_resourcemethodroutesaURLtoaresource,specificallytooneofthepreviouslydeclaredsubclassesoftheflask_restful.Resourceclass.WhenthereisarequesttotheAPIandtheURLmatchesoneoftheURLsspecifiedintheapi.add_resourcemethod,FlaskwillcallthemethodthatmatchestheHTTPverbintherequestforthespecifiedclass.ThemethodfollowsstandardFlaskroutingrules.
Forexample,thefollowinglinewillmakeanHTTPGETrequestto/api/messages/withoutanyadditionalparameterstocalltheMessageList.getmethod:
api.add_resource(MessageList,'/api/messages/')
FlaskwillpasstheURLvariablestothecalledmethodasarguments.Forexample,thefollowinglinewillmakeanHTTPGETrequestto/api/messages/12tocalltheMessage.getmethodwith12passedasthevaluefortheidargument:
api.add_resource(Message,'/api/messages/<int:id>',endpoint='message_endpoint')
Inaddition,wecanspecifyastringvaluefortheendpointargumenttomakeiteasytoreferencethespecifiedrouteinfields.Urlfields.Wepassthesameendpointname,'message_endpoint'asanargumentintheurifielddeclaredasfields.Urlinthemessage_fieldsdictionarythatweusetorendereachMessageModelinstance.Thisway,fields.UrlwillgenerateaURIconsideringthisroute.
Wejustrequiredafewlinesofcodetoconfigureresourceroutingandendpoints.Thelastlinejustcallstheapp.runmethodtostarttheFlaskapplicationwiththedebugargumentsettoTruetoenabledebugging.Inthiscase,westarttheapplicationbycallingtherunmethodtoimmediatelylaunchalocalserver.Wecouldalsoachievethesamegoalbyusingtheflaskcommand-linescript.However,thisoptionwouldrequireustoconfigureenvironment
variablesandtheinstructionsaredifferentfortheplatformsthatwearecoveringinthisbook-macOS,WindowsandLinux.
Tip
AswithanyotherWebframework,youshouldneverenabledebugginginaproductionenvironment.
MakingHTTPrequeststotheFlaskAPINow,wecanruntheapi/api.pyscriptthatlaunchesFlask'sdevelopmentservertocomposeandsendHTTPrequeststoourunsecureandsimpleWebAPI(wewilldefinitelyaddsecuritylater).Executethefollowingcommand.
pythonapi/api.py
Thefollowinglinesshowtheoutputafterweexecutethepreviouscommand.Thedevelopmentserverislisteningatport5000.
*Runningonhttp://127.0.0.1:5000/(PressCTRL+Ctoquit)
*Restartingwithstat
*Debuggerisactive!
*Debuggerpincode:294-714-594
Withthepreviouscommand,wewillstartFlaskdevelopmentserverandwewillonlybeabletoaccessitinourdevelopmentcomputer.ThepreviouscommandstartsthedevelopmentserverinthedefaultIPaddress,thatis,127.0.0.1(localhost).ItisnotpossibletoaccessthisIPaddressfromothercomputersordevicesconnectedonourLAN.Thus,ifwewanttomakeHTTPrequeststoourAPIfromothercomputersordevicesconnectedtoourLAN,weshouldusethedevelopmentcomputerIPaddress,0.0.0.0(forIPv4configurations)or::(forIPv6configurations),asthedesiredIPaddressforourdevelopmentserver.
Ifwespecify0.0.0.0asthedesiredIPaddressforIPv4configurations,thedevelopmentserverwilllistenoneveryinterfaceonport5000.Inaddition,itisnecessarytoopenthedefaultport5000inourfirewalls(softwareand/orhardware)andconfigureport-forwardingtothecomputerthatisrunningthedevelopmentserver.
Wejustneedtospecify'0.0.0.0'asthevalueforthehostargumentinthecalltotheapp.runmethod,specifically,thelastlineintheapi/api.pyfile.Thefollowinglineshowsthenewcalltoapp.runthatlaunchesFlask'sdevelopmentserverinanIPv4configurationandallowsrequeststobemadefromothercomputersanddevicesconnectedtoourLAN.Thelinegeneratesanexternallyvisibleserver.Thecodefileforthesampleisincludedintherestful_python_chapter_05_02folder:
if__name__=='__main__':
app.run(host='0.0.0.0',debug=True)
Tip
IfyoudecidetocomposeandsendHTTPrequestsfromothercomputersordevicesconnectedtotheLAN,rememberthatyouhavetousethedevelopmentcomputer'sassignedIPaddressinsteadoflocalhost.Forexample,ifthecomputer'sassignedIPv4IPaddressis192.168.1.103,insteadoflocalhost:5000,youshoulduse192.168.1.103:5000.Ofcourse,youcanalsousethehostnameinsteadoftheIPaddress.Thepreviouslyexplainedconfigurationsareveryimportantbecausemobiledevicesmightbetheconsumersofour
RESTfulAPIsandwewillalwayswanttotesttheappsthatmakeuseofourAPIsinourdevelopmentenvironments.Inaddition,wecanworkwithusefultoolssuchasngrokthatallowustogeneratesecuretunnelstolocalhost.Youcanreadmoreinformationaboutngrokathttp://www.ngrok.com.
TheFlaskdevelopmentserverisrunningonlocalhost(127.0.0.1),listeningonport5000,andwaitingforourHTTPrequests.Now,wewillcomposeandsendHTTPrequestslocallyinourdevelopmentcomputerorfromothercomputerordevicesconnectedtoourLAN.
Workingwithcommand-linetools–curlandhttpieWewillstartcomposingandsendingHTTPrequestswiththecommand-linetoolswehaveintroducedinChapter1,DevelopingRESTfulAPIswithDjango,curlandHTTPie.Incaseyouhaven'tinstalledHTTPie,makesureyouactivatethevirtualenvironmentandthenrunthefollowingcommandintheterminalorcommandprompttoinstalltheHTTPiepackage.
pipinstall--upgradehttpie
Tip
Incaseyoudon'trememberhowtoactivatethevirtualenvironmentthatwecreatedforthisexample,readthefollowingsectioninthischapter-SettingupthevirtualenvironmentwithDjangoRESTframework.
OpenaCygwinTerminalinWindowsoraTerminalinmacOSorLinux,andrunthefollowingcommand.Itisveryimportantthatyouentertheendingslash(/)whenspecified/api/messageswon'tmatchanyoftheconfiguredURLroutes.Thus,wemustenter/api/messages/,includingtheendingslash(/).WewillcomposeandsendanHTTPrequesttocreateanewmessage:
httpPOST:5000/api/messages/message='WelcometoIoT'duration=10
message_category='Information'
Thefollowingistheequivalentcurlcommand.Itisveryimportanttousethe-H"Content-Type:application/json"optiontoindicatecurltosendthedataspecifiedafterthe-doptionasapplication/jsoninsteadofthedefaultapplication/x-www-form-urlencoded:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Welcometo
IoT","duration":10,"message_category":"Information"}':5000/api/messages/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:POSThttp://localhost:5000/api/messages/withthefollowingJSONkey-valuepairs:
{
"message":"WelcometoIoT",
"duration":10,
"message_category":"Information"
}
Therequestspecifies/api/messages/,andtherefore,itwillmatch'/api/messages/'andruntheMessageList.postmethod.Themethoddoesn'treceiveargumentsbecausetheURLroutedoesn'tincludeanyparameters.AstheHTTPverbfortherequestisPOST,Flaskcallsthepostmethod.IfthenewMessageModelwassuccessfullypersistedinthedictionary,thefunctionreturnsanHTTP201CreatedstatuscodeandtherecentlypersistedMessageModelserializedserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewMessageModelobjectintheJSONresponse:
HTTP/1.0201CREATED
Content-Length:245
Content-Type:application/json
Date:Wed,20Jul201604:43:24GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"creation_date":"Wed,20Jul201604:43:24-0000",
"duration":10,
"id":1,
"message":"WelcometoIoT",
"message_category":"Information",
"printed_once":false,
"printed_times":0,
"uri":"/api/messages/1"
}
WewillcomposeandsendanHTTPrequesttocreateanothermessage.GobacktotheCygwinterminalinWindowsortheTerminalinmacOSorLinux,andrunthefollowingcommand:
httpPOST:5000/api/messages/message='Measuringambienttemperature'
duration=5message_category='Information'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Measuring
ambienttemperature","duration":5,"message_category":"Information"}'
:5000/api/messages/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest,POSThttp://localhost:5000/api/messages/,withthefollowingJSONkey-valuepairs:
{
"message":"Measuringambienttemperature",
"duration":5,
"message_category":"Information"
}
ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewMessageModelobjectintheJSONresponse:
HTTP/1.0201CREATED
Content-Length:259
Content-Type:application/json
Date:Wed,20Jul201618:27:05GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"creation_date":"Wed,20Jul201618:27:05-0000",
"duration":5,
"id":2,
"message":"Measuringambienttemperature",
"message_category":"Information",
"printed_once":false,
"printed_times":0,
"uri":"/api/messages/2"
}
WewillcomposeandsendanHTTPrequesttoretrieveallthemessages.GobacktotheCygwinterminalinWindowsortheTerminalinmacOSorLinux,andrunthefollowingcommand:
http:5000/api/messages/
Thefollowingistheequivalentcurlcommand:
curl-iXGET-H:5000/api/messages/
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:5000/api/messages/.Therequestspecifies/api/messages/,andtherefore,itwillmatch'/api/messages/'andruntheMessageList.getmethod.Themethoddoesn'treceiveargumentsbecausetheURLroutedoesn'tincludeanyparameters.AstheHTTPverbfortherequestisGET,Flaskcallsthegetmethod.ThemethodretrievesalltheMessageModelobjectsandgeneratesaJSONresponsewithalloftheseMessageModelobjectsserialized.
ThefollowinglinesshowanexampleresponsefortheHTTPrequest.ThefirstlinesshowtheHTTPresponseheaders,includingthestatus(200OK)andtheContent-type(application/json).AftertheHTTPresponseheaders,wecanseethedetailsforthetwoMessageModelobjectsintheJSONresponse:
HTTP/1.0200OK
Content-Length:589
Content-Type:application/json
Date:Wed,20Jul201605:32:28GMT
Server:Werkzeug/0.11.10Python/3.5.1
[
{
"creation_date":"Wed,20Jul201605:32:06-0000",
"duration":10,
"id":1,
"message":"WelcometoIoT",
"message_category":"Information",
"printed_once":false,
"printed_times":0,
"uri":"/api/messages/1"
},
{
"creation_date":"Wed,20Jul201605:32:18-0000",
"duration":5,
"id":2,
"message":"Measuringambienttemperature",
"message_category":"Information",
"printed_once":false,
"printed_times":0,
"uri":"/api/messages/2"
}
]
Afterwerunthethreerequests,wewillseethefollowinglinesinthewindowthatisrunningtheFlaskdevelopmentserver.TheoutputindicatesthattheserverreceivedthreeHTTPrequests,specificallytwoPOSTrequestsandoneGETrequestwith/api/messages/astheURI.TheserverprocessedthethreeHTTPrequests,returnedstatuscode201forthefirsttworequestsand200forthelastrequest:
127.0.0.1--[20/Jul/201602:32:06]"POST/api/messages/HTTP/1.1"201-
127.0.0.1--[20/Jul/201602:32:18]"POST/api/messages/HTTP/1.1"201-
127.0.0.1--[20/Jul/201602:32:28]"GET/api/messages/HTTP/1.1"200-
ThefollowingimageshowstwoTerminalwindowsside-by-sideonmacOS.TheTerminalwindowattheleft-handsideisrunningtheFlaskdevelopmentserveranddisplaysthereceivedandprocessedHTTPrequests.TheTerminalwindowattheright-handsideisrunninghttpcommandstogeneratetheHTTPrequests.ItisagoodideatouseasimilarconfigurationtochecktheoutputwhilewecomposeandsendtheHTTPrequests:
Now,wewillcomposeandsendanHTTPrequesttoretrieveamessagethatdoesn'texist.Forexample,inthepreviouslist,thereisnomessagewithanidvalueequalto800.Runthefollowingcommandtotrytoretrievethismessage.Makesureyouuseanidvaluethatdoesn'texist.Wemustmakesurethattheutilitiesdisplaytheheadersaspartoftheresponsetoseethereturnedstatuscode:
http:5000/api/messages/800
Thefollowingistheequivalentcurlcommand:
curl-iXGET:5000/api/messages/800
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:5000/api/messages/800.Therequestisthesamethanthepreviousonewehaveanalyzed,withadifferentnumberfortheidparameter.TheserverwillruntheMessage.getmethodwith800asthevaluefortheidargument.ThemethodwillexecutethecodethatretrievestheMessageModelobjectwhoseidmatchestheidvaluereceivedasanargument.However,thefirstlineintheMessageList.getmethodcallstheabort_if_message_doesnt_existmethodthatwon'tfindtheidinthedictionarykeysanditwillcalltheflask_restful.abortfunctionbecausethereisnomessagewiththespecifiedidvalue.Thus,thecodewillreturnanHTTP404NotFoundstatuscode.ThefollowinglinesshowanexampleheaderresponsefortheHTTPrequestandthemessageincludedinthebody.Inthiscase,wejustleavethedefaultmessage.Ofcourse,wecancustomizeitbasedonourspecificneeds:
HTTP/1.0404NOTFOUND
Content-Length:138
Content-Type:application/json
Date:Wed,20Jul201618:08:04GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"message":"Message800doesn'texist.YouhaverequestedthisURI
[/api/messages/800]butdidyoumean/api/messages/<int:id>?"
}
OurAPIisabletoupdateasinglefieldforanexistingresource,andtherefore,weprovideanimplementationforthePATCHmethod.Forexample,wecanusethePATCHmethodtoupdatetwofieldsforanexistingmessageandsetthevalueforitsprinted_oncefieldtotrueandprinted_timesto1.Wedon'twanttousethePUTmethodbecausethismethodismeanttoreplaceanentiremessage.ThePATCHmethodismeanttoapplyadeltatoanexistingmessage,andtherefore,itistheappropriatemethodtojustchangethevalueoftheprinted_onceandprinted_timesfields.
Now,wewillcomposeandsendanHTTPrequesttoupdateanexistingmessage,specifically,toupdatethevalueoftwofields.Makesureyoureplace2withtheidofanexistingmessageinyourconfiguration:
httpPATCH:5000/api/messages/2printed_once=trueprinted_times=1
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d'{"printed_once":"true",
"printed_times":1}':5000/api/messages/2
ThepreviouscommandwillcomposeandsendaPATCHHTTPrequestwiththespecified
JSONkey-valuepairs.Therequesthasanumberafter/api/messages/,andtherefore,itwillmatch'/api/messages/<int:id>'andruntheMessage.patchmethod,thatis,thepatchmethodfortheMessageclass.IfaMessageModelinstancewiththespecifiedidexistsanditwassuccessfullyupdated,thecalltothemethodwillreturnanHTTP200OKstatuscodeandtherecentlyupdatedMessageModelinstanceserializedtoJSONintheresponsebody.Thefollowinglinesshowasampleresponse:
HTTP/1.0200OK
Content-Length:231
Content-Type:application/json
Date:Wed,20Jul201618:28:01GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"creation_date":"Wed,20Jul201618:27:05-0000",
"duration":0,
"id":2,
"message":"Measuringambienttemperature",
"message_category":"Information",
"printed_once":true,
"printed_times":1,
"uri":"/api/messages/2"
}
Tip
TheIoTdevicewillmakethepreviouslyexplainedHTTPrequestwhenitdisplaysthemessageforthefirsttime.Then,itwillmakeadditionalPATCHrequeststoupdatethevaluefortheprinted_timesfield.
Now,wewillcomposeandsendanHTTPrequesttodeleteanexistingmessage,specifically,thelastmessageweadded.AshappenedinourlastHTTPrequests,wehavetocheckthevalueassignedtoidinthepreviousresponseandreplace2inthecommandwiththereturnedvalue:
httpDELETE:5000/api/messages/2
Thefollowingistheequivalentcurlcommand:
curl-iXDELETE:5000/api/messages/2
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:DELETEhttp://localhost:5000/api/messages/2.Therequesthasanumberafter/api/messages/,andtherefore,itwillmatch'/api/messages/<int:id>'andruntheMessage.deletemethod,thatis,thedeletemethodfortheMessageclass.IfaMessageModelinstancewiththespecifiedidexistsanditwassuccessfullydeleted,thecalltothemethodwillreturnanHTTP204NoContentstatuscode.Thefollowinglinesshowasampleresponse:
HTTP/1.0204NOCONTENT
Content-Length:0
Content-Type:application/json
Date:Wed,20Jul201618:50:12GMT
Server:Werkzeug/0.11.10Python/3.5.1
WorkingwithGUItools-PostmanandothersSofar,wehavebeenworkingwithtwoterminal-basedorcommand-linetoolstocomposeandsendHTTPrequeststoourFlaskdevelopmentserver-cURLandHTTPie.Now,wewillworkwithoneoftheGUItoolsweusedwhencomposingandsendingHTTPrequeststotheDjangodevelopmentserver-Postman.
Now,wewillusetheBuildertabinPostmantoeasilycomposeandsendHTTPrequeststolocalhost:5000andtesttheRESTfulAPIwiththisGUItool.RememberthatPostmandoesn'tsupportcurl-likeshorthandsforlocalhost,andtherefore,wecannotusethesameshorthandswehavebeenusingwhencomposingrequestswithcurlandHTTPie.
SelectGET inthedropdownmenuattheleft-handsideoftheEnterrequestURLtextbox,andenterlocalhost:5000/api/messages/inthistextboxattheright-handsideofthedropdown.Then,clickSendandPostmanwilldisplaytheStatus(200OK),thetimeittookfortherequesttobeprocessedandtheresponsebodywithallthegamesformattedasJSONwithsyntaxhighlighting(Prettyview).ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPGETrequest.
ClickonHeadersattheright-handsideofBodyandCookiestoreadtheresponseheaders.ThefollowingscreenshotshowsthelayoutfortheresponseheadersthatPostmandisplaysfor
thepreviousresponse.NoticethatPostmandisplaystheStatusattheright-handsideoftheresponseanddoesn'tincludeitasthefirstlineoftheHeaders,ashappenedwhenweworkedwithboththecURLandHTTPieutilities:
Now,wewillusetheBuildertabinPostmantocomposeandsendanHTTPrequesttocreateanewmessage,specifically,aPOSTrequest.Followthenextsteps:
1. SelectPOST inthedrop-downmenuattheleft-handsideoftheEnterrequestURLtextbox,andenterlocalhost:5000/api/messages/inthistextboxattheright-handsideofthedropdown.
2. ClickBodyattheright-handsideofAuthorizationandHeaders,withinthepanelthatcomposestherequest.
3. ActivatetherawradiobuttonandselectJSON(application/json)inthedropdownattheright-handsideofthebinaryradiobutton.PostmanwillautomaticallyaddaContent-type=application/jsonheader,andtherefore,youwillnoticetheHeaderstabwillberenamedtoHeaders(1),indicatingusthatthereisonekey-valuepairspecifiedfortherequestheaders.
4. Enterthefollowinglinesinthetextboxbelowtheradiobuttons,withintheBodytab:
{
"message":"Measuringdistance",
"duration":5,
"message_category":"Information"
}
ThefollowingscreenshotshowstherequestbodyinPostman:
WefollowedthenecessarystepstocreateanHTTPPOSTrequestwithaJSONbodythatspecifiesthenecessarykey-valuepairstocreateanewgame.ClickSendandPostmanwilldisplaytheStatus(201Created),thetimeittookfortherequesttobeprocessedandtheresponsebodywiththerecentlyaddedgameformattedasJSONwithsyntaxhighlighting(Prettyview).ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPPOSTrequest:
Tip
IfwewanttocomposeandsendanHTTPPATCHrequestforourAPIwithPostman,itisnecessarytofollowthepreviouslyexplainedstepstoprovideJSONdatawithintherequestbody.
ClickortaponthevaluefortheurlfieldintheJSONresponsebody-/api/messages/2.You
willnoticethatthevaluewillbeunderlinedwhenyouhoverthemousepointeroverit.PostmanwillautomaticallygenerateaGETrequesttolocalhost:5000/api/messages/2.ClickSendtorunitandretrievetherecentlyaddedmessage.ThefieldisusefultobrowsetheAPIwithatoolsuchasPostman.
BecausewemadethenecessarychangestogenerateanexternallyvisibleFlaskdevelopmentserver,wecanalsouseappsthatcancomposeandsendHTTPrequestsfrommobiledevicestoworkwiththeRESTfulAPI.Forexample,wecanworkwiththeiCurlHTTPApponiOSdevicessuchasiPadProandiPhone.InAndroiddevices,wecanworkwiththepreviouslyintroducedHTTPRequestApp.
ThefollowingscreenshotshowstheresultsofcomposingandsendingthefollowingHTTPrequestwiththeiCurlHTTPApp:GEThttp://192.168.2.3:5000/api/messages/.RememberthatyouhavetoperformthepreviouslyexplainedconfigurationsinyourLANandroutertobeabletoaccesstheFlaskdevelopmentserverfromotherdevicesconnectedtoyourLAN.Inthiscase,theIPassignedtothecomputerrunningtheFlaskWebserveris192.168.2.3,andtherefore,youmustreplacethisIPwiththeIPassignedtoyourdevelopmentcomputer.
Testyourknowledge1. Flask-RESTfuluseswhichofthefollowingasthemainbuildingblockforaRESTful
API?1. ResourcesbuiltontopofFlaskpluggableviews2. StatusesbuiltontopofFlaskresourceviews.3. ResourcesbuiltontopofFlaskpluggablecontrollers.
2. InordertobeabletoprocessanHTTPPOSTrequestonaresource,wemustdeclareamethodwiththefollowingnameinasubclassofflask_restful.Resource.1. post_restful2. post_method3. post
3. InordertobeabletoprocessanHTTPGETrequestonaresource,wemustdeclareamethodwiththefollowingnameinasubclassofflask_restful.Resource.1. get_restful2. get_method3. get
4. Asubclassofflask_restful.Resourcerepresents:1. Acontrollerresource.2. ARESTfulresource.3. AsingleRESTfulHTTPverb.
5. Ifweusethe@marshal_withdecoratorwithmessage_fieldsasanargument,thedecoratorwill:1. Applythefieldfilteringandoutputformattingspecifiedinmessage_fieldstothe
appropriateinstance.2. Applythefieldfilteringspecifiedinmessage_fieldstotheappropriateinstance,
withoutconsideringoutputformatting.3. Applytheoutputformattingspecifiedinmessage_fieldstotheappropriateinstance,
withoutconsideringfieldfiltering.
SummaryInthischapter,wedesignedaRESTfulAPItointeractwithasimpledictionarythatactedasadatarepositoryandperformCRUDoperationswithmessages.WedefinedtherequirementsforourAPIandweunderstoodthetasksperformedbyeachHTTPmethod.WesetupavirtualenvironmentwithFlaskandFlask-RESTful.
Wecreatedamodeltorepresentandpersistmessages.WelearnedtoconfigureserializationofmessagesintoJSONrepresentationswiththefeaturesincludedinFlask-RESTful.WewroteclassesthatrepresentresourcesandprocessthedifferentHTTPrequestsandweconfiguredtheURLpatternstorouteURLstoclasses.
Finally,westartedFlaskdevelopmentserverandweusedcommand-linetoolstocomposeandsendHTTPrequeststoourRESTfulAPIandanalyzedhoweachHTTPrequestwasprocessedinourcode.WealsoworkedwithGUItoolstocomposeandsendHTTPrequests.
NowthatweunderstandthebasicsofthecombinationofFlaskandFlask-RESTfultocreateRESTfulAPIs,wewillexpandthecapabilitiesoftheRESTfulWebAPIbytakingadvantageofadvancedfeaturesincludedinFlask-RESTfulandrelatedORMs,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter6.WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlaskInthischapter,wewillexpandthecapabilitiesoftheRESTfulAPIthatwestartedinthepreviouschapter.WewilluseSQLAlchemyasourORMtoworkwithaPostgreSQLdatabaseandwewilltakeadvantageofadvancedfeaturesincludedinFlaskandFlask-RESTfulthatwillallowustoeasilyorganizecodeforcomplexAPIs,suchasmodelsandblueprints.Inthischapter,wewill:
DesignaRESTfulAPItointeractwithaPostgreSQLdatabaseUnderstandthetasksperformedbyeachHTTPmethodInstallpackagestosimplifyourcommontasksCreateandconfigurethedatabaseWritecodeforthemodelswiththeirrelationshipsUseschemastovalidate,serialize,anddeserializemodelsCombineblueprintswithresourcefulroutingRegistertheblueprintandrunmigrationsCreateandretrieverelatedresources
DesigningaRESTfulAPItointeractwithaPostgreSQLdatabaseSofar,ourRESTfulAPIhasperformedCRUDoperationsonasimpledictionarythatactedasadatarepository.Now,wewanttocreateamorecomplexRESTfulAPIwithFlaskRESTfultointeractwithadatabasemodelthathastoallowustoworkwithmessagesthataregroupedintomessagecategories.InourpreviousRESTfulAPI,weusedastringattributetospecifythemessagecategoryforamessage.Inthiscase,wewanttobeabletoeasilyretrieveallthemessagesthatbelongtoaspecificmessagecategory,andtherefore,wewillhavearelationshipbetweenamessageandamessagecategory.
WemustbeabletoperformCRUDoperationsondifferentrelatedresourcesandresourcecollections.Thefollowinglistenumeratestheresourcesandtheclassnamethatwewillcreatetorepresentthemodel:
Messagecategories(Categorymodel)Messages(Messagemodel)
Themessagecategory(Category)justrequiresanintegername,andweneedthefollowingdataforamessage(Message):
AnintegeridentifierAforeignkeytoamessagecategory(Category)AstringmessageThedurationinsecondsthatwillindicatethetimethemessagehastobeprintedontheOLEDdisplayThecreationdateandtime.ThetimestampwillbeaddedautomaticallywhenaddinganewmessagetothecollectionAnintegercounterthatindicatesthetimesthemessagehasbeenprintedintheOLEDdisplayAboolvalueindicatingwhetherthemessagewasprintedatleastonceontheOLEDdisplay
Tip
WewilltakeadvantageofthemanypackagesrelatedtoFlaskRESTfulandSQLAlchemythatmakeiteasiertoserializeanddeserializedata,performvalidations,andintegrateSQLAlchemywithFlaskandFlaskRESTful.
UnderstandingthetasksperformedbyeachHTTPmethodThefollowingtableshowstheHTTPverbs,thescope,andthesemanticsforthemethodsthatournewAPImustsupport.EachmethodiscomposedofanHTTPverb,ascope,andallthemethodshavewell-definedmeaningsforalltheresourcesandcollections:
HTTPverb Scope Semantics
GET
Collectionofmessagecategories
Retrieveallthestoredmessagecategoriesinthecollectionandreturnthemsortedbytheirnameinascendingorder.EachcategorymustincludethefullURLfortheresource.Eachcategorymustincludealistwithallthedetailsforthemessagesthatbelongtothecategory.Themessagesdon'thavetoincludethecategoryinordertoavoidrepeatingdata.
GETMessagecategory
Retrieveasinglemessagecategory.Thecategorymustincludethesameinformationexplainedforeachcategorywhenweretrieveacollectionofmessagecategory.
POST
Collectionofmessagecategories
Createanewmessagecategoryinthecollection.
PATCHMessagecategory Updatethenameofanexistingmessagecategory.
DELETEMessagecategory Deleteanexistingmessagecategory.
GET
Collectionofmessages
Retrieveallthestoredmessagesinthecollection,sortedbytheirmessageinascendingorder.Eachmessagemustincludeitsmessagecategorydetails,includingthefullURLtoaccesstherelatedresource.Themessagecategorydetailsdon'thavetoincludethemessagesthatbelongtothecategory.ThemessagemustincludethefullURLtoaccesstheresource.
GET MessageRetrieveasinglemessage.Themessagemustincludethesameinformationexplainedforeachmessagewhenweretrieveacollectionofmessages.
POST
Collectionofmessages
Createanewmessageinthecollection.
PATCH Message Updateanyofthefollowingfieldsofanexistingmessage:message,duration,printed_times,andprinted_once.
DELETE Message Deleteanexistingmessage.
Inaddition,ourRESTfulAPImustsupporttheOPTIONSmethodforalltheresourcesandcollectionofresources.WewilluseSQLAlchemyasourORMandwewillworkwithaPostgreSQLdatabase.However,incaseyoudon'twanttospendtimeinstallingPostgreSQL,youcanuseanyotherdatabasesupportedbySQLAlchemy,suchasMySQL.Incaseyouwantthesimplestdatabase,youcanworkwithSQLite.
Intheprecedingtable,therearemanymethodsandscopes.ThefollowinglistenumeratestheURIsforeachscopementionedintheprecedingtable,where{id}hastobereplacedwiththenumericidorprimarykeyoftheresource.Ashappenedinthepreviousexample,wewantourAPItodifferentiatecollectionsfromasingleresourceofthecollectionintheURLs.Whenwerefertoacollection,wewilluseaslash(/)asthelastcharacterfortheURLandwhenwerefertoasingleresourceofthecollection,wewon'tuseaslash(/)asthelastcharacterfortheURL:
Collectionofmessagecategories:/categories/Messagecategory:/category/{id}Collectionofmessages:/messages/Message:/message/{id}
Let'sconsiderthathttp://localhost:5000/api/istheURLfortheAPIrunningontheFlaskdevelopmentserver.WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:5000/api/categories/)toretrieveallthestoredmessagecategoriesinthecollection.Eachcategorywillincludealistwithallthemessagesthatbelongtothecategory.
GEThttp://localhost:5000/api/categories/
InstallingpackagestosimplifyourcommontasksMakesureyouquitFlask'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheTerminalorCommandPromptwindowinwhichitisrunning.Now,wewillinstallmanyadditionalpackages.MakesureyouhaveactivatedthevirtualenvironmentwehavecreatedinthepreviouschapterandwenamedFlask01.Incaseyoucreatedanewvirtualenvironmenttoworkwiththisexampleoryoudownloadedthesamplecodeforthebook,makesureyouinstallthepackagesweusedinthepreviousexample.
Afteryouactivatethevirtualenvironment,itistimetoruncommandsthatwillbethesameforeithermacOS,Linux,orWindows.Wecaninstallallthenecessarypackageswithpipwithasinglecommand.However,wewillrunindependentcommandstomakeiteasiertodetectanyproblemsincaseaspecificinstallationfails.
Now,wemustrunthefollowingcommandtoinstallFlask-SQLAlchemywithpip.Flask-SQLAlchemyaddssupportfortheSQLAlchemyORMtoFlaskapplications.ThisextensionsimplifiesexecutingcommonSQLAlchemytaskswithinaFlaskapplication.SQLAlchemyisadependencyforFlask-SQLAlchemy,andtherefore,pipwillinstallitautomatically,too:
pipinstallFlask-SQLAlchemy
Thelastlinesoftheoutputwillindicateallthepackagesthathavebeensuccessfullyinstalled,includingSQLAlchemyandFlask-SQLAlchemy:
Installingcollectedpackages:SQLAlchemy,Flask-SQLAlchemy
Runningsetup.pyinstallforSQLAlchemy
Runningsetup.pyinstallforFlask-SQLAlchemy
SuccessfullyinstalledFlask-SQLAlchemy-2.1SQLAlchemy-1.0.14
RunthefollowingcommandtoinstallFlask-Migratewithpip.Flask-MigrateusestheAlembicpackagetohandleSQLAlchemydatabasemigrationsforFlaskapplications.WewilluseFlask-MigratetosetupourPostgreSQLdatabase.Flask-ScriptisoneofthedependenciesforFlask-Migrate,andtherefore,pipwillinstallitautomatically.Flask-ScriptaddssupportforwritingexternalscriptsinFlask,includingscriptstosetupadatabase.
pipinstallFlask-Migrate
Thelastlinesfortheoutputwillindicateallthepackagesthathavebeensuccessfullyinstalled,includingFlask-MigrateandFlask-Script.Theotherinstalledpackagesareadditionaldependencies:
Installingcollectedpackages:Mako,python-editor,alembic,Flask-Script,
Flask-Migrate
Runningsetup.pyinstallforMako
Runningsetup.pyinstallforpython-editor
Runningsetup.pyinstallforalembic
Runningsetup.pyinstallforFlask-Script
Runningsetup.pyinstallforFlask-Migrate
SuccessfullyinstalledFlask-Migrate-2.0.0Flask-Script-2.0.5Mako-1.0.4
alembic-0.8.7python-editor-1.0.1
Runthefollowingcommandtoinstallmarshmallowwithpip.MarshmallowisalightweightlibraryforconvertingcomplexdatatypestoandfromnativePythondatatypes.Marshmallowprovidesschemasthatwecanusetovalidateinputdata,deserializeinputdatatoapp-levelobjects,andserializeapp-levelobjectstoPythonprimitivetypes:
pipinstallmarshmallow
Thelastlinesfortheoutputwillindicatemarshmallowhasbeensuccessfullyinstalled:
Installingcollectedpackages:marshmallow
Successfullyinstalledmarshmallow-2.9.1
RunthefollowingcommandtoinstallMarshmallow-sqlalchemywithpip.Marshmallow-sqlalchemyprovidesSQLAlchemyintegrationwiththepreviouslyinstalledmarshmallowvalidation,serialization,anddeserializationlightweightlibrary:
pipinstallmarshmallow-sqlalchemy
Thelastlinesfortheoutputwillindicatemarshmallow-sqlalchemyhasbeensuccessfullyinstalled:
Installingcollectedpackages:marshmallow-sqlalchemy
Successfullyinstalledmarshmallow-sqlalchemy-0.10.0
Finally,runthefollowingcommandtoinstallFlask-Marshmallowwithpip.Flask-MarshmallowintegratesthepreviouslyinstalledmarshmallowlibrarywithFlaskapplicationsandmakesiteasytogenerateaURLandHyperlinkfields:
pipinstallFlask-Marshmallow
ThelastlinesfortheoutputwillindicateFlask-Marshmallowhasbeensuccessfullyinstalled:
Installingcollectedpackages:Flask-Marshmallow
SuccessfullyinstalledFlask-Marshmallow-0.7.0
CreatingandconfiguringthedatabaseNow,wewillcreatethePostgreSQLdatabasethatwewilluseasarepositoryforourAPI.YouwillhavetodownloadandinstallaPostgreSQLdatabaseincaseyouaren'talreadyrunningitinyourcomputerorinadevelopmentserver.Youcandownloadandinstallthisdatabasemanagementsystemfromitswebpage:http://www.postgresql.org.IncaseyouareworkingwithmacOS,Postgres.appprovidesareallyeasywaytoinstallandusePostgreSQLonthisoperatingsystem:http://postgresapp.com:
Tip
YouhavetomakesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable.Youshouldbeabletoexecutethepsqlcommand-lineutilityfromyourcurrentTerminalorCommandPrompt.Incasethefolderisn'tincludedinthePATH,youwillreceiveanerrorindicatingthatthepg_configfilecannotbefoundwhentryingtoinstallthepsycopg2package.Inaddition,youwillhavetousethefullpathtoeachofthePostgreSQLcommand-linetoolswewilluseinthenextsteps.
WewillusethePostgreSQLcommand-linetoolstocreateanewdatabasenamedmessages.IncaseyoualreadyhaveaPostgreSQLdatabasewiththisname,makesurethatyouuseanothernameinallthecommandsandconfigurations.YoucanperformthesametaskwithanyPostgreSQLGUItool.IncaseyouaredevelopingonLinux,itisnecessarytorunthecommandsasthepostgresuser.RunthefollowingcommandinmacOSorWindowstocreateanewdatabasenamedmessages.Notethatthecommandwon'tproduceanyoutput:
createdbmessages
InLinux,runthefollowingcommandtousethepostgresuser:
sudo-upostgrescreatedbmessages
Now,wewillusethepsqlcommand-linetooltorunsomeSQLstatementstocreateaspecificuserthatwewilluseinFlaskandassignthenecessaryrolesforit.InmacOSorWindows,runthefollowingcommandtolaunchpsql:
psql
InLinux,runthefollowingcommandtousethepostgresuser:
sudo-upsql
Then,runthefollowingSQLstatementsandfinallyenter\qtoexitthepsqlcommand-linetool.Replaceuser_namewithyourdesiredusernametouseinthenewdatabaseandpasswordwithyourchosenpassword.WewillusetheusernameandpasswordintheFlaskconfiguration.Youdon'tneedtorunthestepsincaseyouarealreadyworkingwithaspecificuserinPostgreSQLandyouhavealreadygrantedprivilegestothedatabasefortheuser.Youwillseetheoutputindicatingthatthepermissionwasgranted.
CREATEROLEuser_nameWITHLOGINPASSWORD'password';
GRANTALLPRIVILEGESONDATABASEmessagesTOuser_name;
ALTERUSERuser_nameCREATEDB;
\q
ItisnecessarytoinstallthePsycopg2package(psycopg2).ThispackageisaPython-PostgreSQLDatabaseAdapterandSQLAlchemywilluseittointeractwithourrecentlycreatedPostgreSQLdatabase.
OncewemadesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable,wejustneedtorunthefollowingcommandtoinstallthispackage:
pipinstallpsycopg2
Thelastlinesoftheoutputwillindicatethatthepsycopg2packagehasbeensuccessfullyinstalled:
Collectingpsycopg2
Installingcollectedpackages:psycopg2
Runningsetup.pyinstallforpsycopg2
Successfullyinstalledpsycopg2-2.6.2
Incaseyouareusingthesamevirtualenvironmentthatwecreatedforthepreviousexample,theapifolderalreadyexists.Ifyoucreateanewvirtualenvironment,createafoldernamedapiwithintherootfolderforthecreatedvirtualenvironment.
Createanewconfig.pyfilewithintheapifolder.ThefollowinglinesshowthecodethatdeclaresvariablesthatdeterminetheconfigurationforFlaskandSQLAlchemy.TheSQL_ALCHEMY_DATABASE_URIvariablegeneratesanSQLAlchemyURIforthePostgreSQLdatabase.
MakesureyouspecifythedesireddatabasenameinthevalueforDB_NAMEandthatyouconfiguretheuser,password,host,andportbasedonyourPostgreSQLconfiguration.Incaseyoufollowedtheprevioussteps,usethesettingsspecifiedinthesesteps.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
importos
basedir=os.path.abspath(os.path.dirname(__file__))
DEBUG=True
PORT=5000
HOST="127.0.0.1"
SQLALCHEMY_ECHO=False
SQLALCHEMY_TRACK_MODIFICATIONS=True
SQLALCHEMY_DATABASE_URI="postgresql://{DB_USER}:
{DB_PASS}@{DB_ADDR}/{DB_NAME}".format(DB_USER="user_name",DB_PASS="password",
DB_ADDR="127.0.0.1",DB_NAME="messages")
SQLALCHEMY_MIGRATE_REPO=os.path.join(basedir,'db_repository')
WewillspecifythemodulecreatedearlierasanargumenttoafunctionthatwillcreateaFlaskapp.Thisway,wehaveonemodulethatspecifiesallthevaluesforthedifferentconfigurationvariablesandanothermodulethatcreatesaFlaskapp.WewillcreatetheFlaskappfactoryasourfinalsteptowardsournewAPI.
CreatingmodelswiththeirrelationshipsNow,wewillcreatethemodelsthatwecanusetorepresentandpersistthemessagecategories,messages,andtheirrelationships.Opentheapi/models.pyfileandreplaceitscontentswiththefollowingcode.Thelinesthatdeclarefieldsrelatedtoothermodelsarehighlightedinthecodelisting.Incaseyoucreatedanewvirtualenvironment,createanewmodels.pyfilewithintheapifolder.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
frommarshmallowimportSchema,fields,pre_load
frommarshmallowimportvalidate
fromflask_sqlalchemyimportSQLAlchemy
fromflask_marshmallowimportMarshmallow
db=SQLAlchemy()
ma=Marshmallow()
classAddUpdateDelete():
defadd(self,resource):
db.session.add(resource)
returndb.session.commit()
defupdate(self):
returndb.session.commit()
defdelete(self,resource):
db.session.delete(resource)
returndb.session.commit()
classMessage(db.Model,AddUpdateDelete):
id=db.Column(db.Integer,primary_key=True)
message=db.Column(db.String(250),unique=True,nullable=False)
duration=db.Column(db.Integer,nullable=False)
creation_date=db.Column(db.TIMESTAMP,
server_default=db.func.current_timestamp(),nullable=False)
category_id=db.Column(db.Integer,db.ForeignKey('category.id',
ondelete='CASCADE'),nullable=False)
category=db.relationship('Category',backref=db.backref('messages',
lazy='dynamic',order_by='Message.message'))
printed_times=db.Column(db.Integer,nullable=False,server_default='0')
printed_once=db.Column(db.Boolean,nullable=False,server_default='false')
def__init__(self,message,duration,category):
self.message=message
self.duration=duration
self.category=category
classCategory(db.Model,AddUpdateDelete):
id=db.Column(db.Integer,primary_key=True)
name=db.Column(db.String(150),unique=True,nullable=False)
def__init__(self,name):
self.name=name
First,thecodecreatesaninstanceoftheflask_sqlalchemy.SQLAlchemyclassnameddb.ThisinstancewillallowustocontroltheSQLAlchemyintegrationforourFlaskapplication.Inaddition,theinstancewillprovideaccesstoalltheSQLAlchemyfunctionsandclasses.
Then,thecodecreatesaninstanceoftheflask_marshmallow.Marshmallowclassnamedma.Itisveryimportanttocreatetheflask_sqlalchemy.SQLAlchemyinstancebeforetheMarshmallowinstance,andtherefore,ordermattersinthiscase.MarshmallowisawrapperclassthatintegratesMashmallowwithaFlaskapplication.TheinstancenamedmawillprovideaccesstotheSchemaclass,thefieldsdefinedinmarshmallow.fields,andtheFlask-specificfieldsdeclaredinflask_marshmallow.fields.Wewillusethemlaterwhenwedeclaretheschemasrelatedtoourmodels.
ThecodecreatestheAddUpdateDeleteclassthatdeclaresthefollowingthreemethodstoadd,update,anddeletearesourcethroughSQLAlchemysessions:
add:Thismethodreceivestheobjecttobeaddedintheresourceargumentandcallsthedb.session.addmethodwiththereceivedresourceasanargumenttocreatetheobjectintheunderlyingdatabase.Finally,thecodecommitsthesession.update:Thismethodjustcommitsthesessiontopersistthechangesmadetotheobjectsintheunderlyingdatabase.delete:Thismethodreceivestheobjecttobedeletedintheresourceargumentandcallsthedb.session.deletemethodwiththereceivedresourceasanargumenttoremovetheobjectintheunderlyingdatabase.Finally,thecodecommitsthesession.
Thecodedeclaresthefollowingtwomodels,specifically,twoclasses,asasubclassofboththedb.Model,andtheAddUpdateDeleteclasses:
Message
Category
Wespecifiedthefieldtypes,maximumlengths,anddefaultsformanyattributes.Theattributesthatrepresentfieldswithoutanyrelationshipareinstancesofthedb.Columnclass.BothmodelsdeclareanidattributeandspecifytheTruevaluefortheprimary_keyargumenttoindicateitistheprimarykey.SQLAlchemywillusethedatatogeneratethenecessarytablesinthePostgreSQLdatabase.
TheMessagemodeldeclaresthecategoryfieldwiththefollowingline:
category=db.relationship('Category',backref=db.backref('messages',
lazy='dynamic',order_by='Message.message'))
Thepreviouslineusesthedb.relationshipfunctiontoprovideamany-to-onerelationshiptotheCategorymodel.Thebackrefargumentspecifiesacalltothedb.backreffunctionwith
'messages'asthefirstvaluethatindicatesthenametousefortherelationfromtherelatedCategoryobjectbacktoaMessageobject.Theorder_byargumentspecifies'Message.message'becausewewantthemessagesforeachcategorytobesortedbythevalueofthemessagefieldinascendingorder.
Bothmodelsdeclareaconstructor,thatis,the__init__method.ThisconstructorfortheMessagemodelreceivesmanyargumentsandusesthemtoinitializetheattributeswiththesamenames:message,duration,andcategory.TheconstructorfortheCategorymodelreceivesanameargumentandusesittoinitializetheattributewiththesamename.
Creatingschemastovalidate,serialize,anddeserializemodelsNow,wewillcreatetheFlask-Marshmallowschemasthatwewillusetovalidate,serialize,anddeserializethepreviouslydeclaredCategoryandMessagemodelsandtheirrelationships.Opentheapi/models.pyfileandaddthefollowingcodeaftertheexistinglines.Thelinesthatdeclarethefieldsrelatedtotheotherschemasarehighlightedinthecodelisting.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
classCategorySchema(ma.Schema):
id=fields.Integer(dump_only=True)
name=fields.String(required=True,validate=validate.Length(3))
url=ma.URLFor('api.categoryresource',id='<id>',_external=True)
messages=fields.Nested('MessageSchema',many=True,exclude=
('category',))
classMessageSchema(ma.Schema):
id=fields.Integer(dump_only=True)
message=fields.String(required=True,validate=validate.Length(1))
duration=fields.Integer()
creation_date=fields.DateTime()
category=fields.Nested(CategorySchema,only=['id','url','name'],
required=True)
printed_times=fields.Integer()
printed_once=fields.Boolean()
url=ma.URLFor('api.messageresource',id='<id>',_external=True)
@pre_load
defprocess_category(self,data):
category=data.get('category')
ifcategory:
ifisinstance(category,dict):
category_name=category.get('name')
else:
category_name=category
category_dict=dict(name=category_name)
else:
category_dict={}
data['category']=category_dict
returndata
Thecodedeclaresthefollowingtwoschemas,specifically,twosubclassesofthema.Schemaclass:
CategorySchema
MessageSchema
Wedon'tusetheFlask-Marshmallowfeaturesthatallowustoautomaticallydeterminetheappropriatetypeforeachattributebasedonthefieldsdeclaredinamodelbecausewewanttousespecificoptionsforeachfield.Wedeclaretheattributesthatrepresentfieldsasinstances
oftheappropriateclassdeclaredinthemarshmallow.fieldsmodule.WheneverwespecifytheTruevalueforthedump_onlyargument,itmeansthatwewantthefieldtoberead-only.Forexample,wewon'tbeabletoprovideavaluefortheidfieldinanyoftheschemas.Thevalueforthisfieldwillbeautomaticallygeneratedbytheauto-incrementprimarykeyinthedatabase.
TheCategorySchemaclassdeclaresthenameattributeasaninstanceoffields.String.TherequiredargumentissettoTruetospecifythatthefieldcannotbeanemptystring.Thevalidateargumentissettovalidate.Length(3)tospecifythatthefieldmusthaveaminimumlengthof3characters.
Theclassdeclarestheurlfieldwiththefollowingline:
url=ma.URLFor('api.categoryresource',id='<id>',_external=True)
Theurlattributeisaninstanceofthema.URLForclass,andthisfieldwilloutputthefullURLoftheresource,thatis,ofthemessagecategoryresource.ThefirstargumentistheFlaskendpointname-'api.categoryresource'.WewillcreateaCategoryResourceclasslaterandtheURLForclasswilluseittogeneratetheURL.Theidargumentspecifies'<id>'becausewewanttheidtobepulledfromtheobjecttobeserialized.Theidstringenclosedwithinlessthan(<)andgreaterthan(>)symbolsspecifiesthatwewantthefieldtobepulledfromtheobjectthathastobeserialized.The_externalattributeissettoTruebecausewewanttogeneratethefullURLfortheresource.Thisway,eachtimeweserializeaCategory,itwillincludethefullURLfortheresourceintheurlkey.
Tip
Inthiscase,weareusingourinsecureAPIbehindHTTP.IncaseourAPIisconfiguredwithHTTPS,weshouldsetthe_schemeargumentto'https'whenwecreatethema.URLForinstance.
Theclassdeclaresthemessagesfieldwiththefollowingline:
messages=fields.Nested('MessageSchema',many=True,exclude=('category',)0029
Themessagesattributeisaninstanceofthemarshmallow.fields.Nestedclass,andthisfieldwillnestacollectionofSchema,andtherefore,wespecifyTrueforthemanyargument.ThefirstargumentspecifiesthenameforthenestedSchemaclassasastring.WedeclaretheMessageSchemaclassafterwedefinedtheCategorySchemaclass.Thus,wespecifytheSchemaclassnameasastringinsteadofusingthetypethatwehaven'tdefinedyet.
Infact,wewillendupwithtwoobjectsthatnesttoeachother,thatis,wewillcreateatwo-waynestingbetweencategoriesandmessages.Weusetheexcludeparameterwithatupleofstringtoindicatethatwewantthecategoryfieldtobeexcludedfromthefieldsthatareserializedforeachmessage.Thisway,wecanavoidinfiniterecursionbecausetheinclusionofthecategoryfieldwouldserializeallthemessagesrelatedtothecategory.
WhenwedeclaredtheMessagemodel,weusedthedb.relationshipfunctiontoprovideamany-to-onerelationshiptotheCategorymodel.Thebackrefargumentspecifiedacalltothedb.backreffunctionwith'messages'asthefirstvaluethatindicatesthenametousefortherelationfromtherelatedCategoryobjectbacktoaMessageobject.Withthepreviouslyexplainedline,wecreatedthemessagesfieldsthatusesthesamenameweindicatedforthedb.backreffunction.
TheMessageSchemaclassdeclaresthemessageattributeasaninstanceoffields.String.TherequiredargumentissettoTruetospecifythatthefieldcannotbeanemptystring.Thevalidateargumentissettovalidate.Length(1)tospecifythatthefieldmusthaveaminimumlengthof1character.Theclassdeclarestheduration,creation_date,printed_timesandprinted_oncefieldswiththecorrespondingclassesbasedonthetypesweusedintheMessagemodel.
Theclassdeclaresthecategoryfieldwiththefollowingline:
category=fields.Nested(CategorySchema,only=['id','url','name'],
required=True)
Thecategoryattributeisaninstanceofthemarshmallow.fields.NestedclassandthisfieldwillnestasingleCategorySchema.WespecifyTruefortherequiredargumentbecauseamessagemustbelongtoacategory.ThefirstargumentspecifiesthenameforthenestedSchemaclass.WealreadydeclaredtheCategorySchemaclass,andtherefore,wespecifyCategorySchemaasthevalueforthefirstargument.WeusetheonlyparameterwithalistofstringtoindicatethefieldnamesthatwewanttobeincludedwhenthenestedCategorySchemaisserialized.Wewanttheid,url,andnamefieldstobeincluded.Wedon'tspecifythemessagesfieldbecausewedon'twantthecategorytoserializethelistofmessagesthatbelongtoit.
Theclassdeclarestheurlfieldwiththefollowingline:
url=ma.URLFor('api.messageresource',id='<id>',_external=True)
Theurlattributeisaninstanceofthema.URLForclassandthisfieldwilloutputthefullURLoftheresource,thatis,ofthemessageresource.ThefirstargumentistheFlaskendpointname:'api.messageresource'.WewillcreateaMessageResourceclasslaterandtheURLForclasswilluseittogeneratetheURL.Theidargumentspecifies'<id>'becausewewanttheidtobepulledfromtheobjecttobeserialized.The_externalattributeissettoTruebecausewewanttogeneratethefullURLfortheresource.Thisway,eachtimeweserializeaMessage,itwillincludethefullURLfortheresourceintheurlkey.
TheMessageSchemaclassdeclaresaprocess_categorymethodthatusesthe@pre_loaddecorator,specifically,marshmallow.pre_load.Thisdecoratorregistersamethodtoinvokebeforedeserializinganobject.Thisway,beforeMarshmallowdeserializesamessage,theprocess_categorymethodwillbeexecuted.
Themethodreceivesthedatatobedeserializedinthedataargumentanditreturnstheprocesseddata.WhenwereceivearequesttoPOSTanewmessage,thecategorynamecanbespecifiedinakeynamed'category'.Ifacategorywiththespecifiednameexists,wewillusetheexistingcategoryastheonethatisrelatedtothenewmessage.Ifacategorywiththespecifiednamedoesn'texist,wewillcreateanewcategoryandthenwewillusethisnewcategoryastheonethatisrelatedtothenewmessage.Thisway,wemakeiteasyfortheusertocreatenewmessages.
Thedataargumentmighthaveacategorynamespecifiedasastringforthe'category'key.However,inothercases,the'category'keywillincludethekey-valuepairswiththefieldnameandfieldvaluesforanexistingcategory.Thecodeintheprocess_categorymethodchecksthevalueforthe'category'keyandreturnsadictionarywiththeappropriatedatatomakeitsurethatweareabletodeserializeacategorywiththeappropriatekey-valuepairs,nomatterthedifferencesoftheincomingdata.Finally,themethodsreturnedtheprocesseddictionary.Wewilldivedeepontheworkdonebytheprocess_categorymethodlaterwhenwestartcomposingandsendingHTTPrequeststotheAPI.
CombiningblueprintswithresourcefulroutingNow,wewillcreatetheresourcesthatcomposeourmainbuildingblocksfortheRESTfulAPI.First,wewillcreateafewinstancesthatwewilluseinthedifferentresources.Then,wewillcreateaMessageResourceclass,thatwewillusetorepresentthemessageresource.Createanewviews.pyfilewithintheapifolderandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder,asshown:
fromflaskimportBlueprint,request,jsonify,make_response
fromflask_restfulimportApi,Resource
frommodelsimportdb,Category,CategorySchema,Message,MessageSchema
fromsqlalchemy.excimportSQLAlchemyError
importstatus
api_bp=Blueprint('api',__name__)
category_schema=CategorySchema()
message_schema=MessageSchema()
api=Api(api_bp)
classMessageResource(Resource):
defget(self,id):
message=Message.query.get_or_404(id)
result=message_schema.dump(message).data
returnresult
defpatch(self,id):
message=Message.query.get_or_404(id)
message_dict=request.get_json(force=True)
if'message'inmessage_dict:
message.message=message_dict['message']
if'duration'inmessage_dict:
message.duration=message_dict['duration']
if'printed_times'inmessage_dict:
message.printed_times=message_dict['printed_times']
if'printed_once'inmessage_dict:
message.printed_once=message_dict['printed_once']
dumped_message,dump_errors=message_schema.dump(message)
ifdump_errors:
returndump_errors,status.HTTP_400_BAD_REQUEST
validate_errors=message_schema.validate(dumped_message)
#errors=message_schema.validate(data)
ifvalidate_errors:
returnvalidate_errors,status.HTTP_400_BAD_REQUEST
try:
message.update()
returnself.get(id)
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_400_BAD_REQUEST
defdelete(self,id):
message=Message.query.get_or_404(id)
try:
delete=message.delete(message)
response=make_response()
returnresponse,status.HTTP_204_NO_CONTENT
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_401_UNAUTHORIZED
Thefirstlinesdeclaretheimportsandcreatethefollowinginstancesthatwewilluseinthedifferentclasses:
api_bp:Itisaninstanceoftheflask.BlueprintclassthatwillallowustofactortheFlaskapplicationintothisblueprint.ThefirstargumentspecifiestheURLprefixonwhichwewanttoregistertheblueprint:'api'.category_schema:ItisaninstanceoftheCategorySchemaclasswedeclaredinthemodels.pymodule.Wewillusecategory_schematovalidate,serialize,anddeserializecategories.message_schema:ItisaninstanceoftheMessageSchemaclasswedeclaredinthemodels.pymodule.Wewillusemessage_schematovalidate,serializeand,deserializecategories.api:Itisaninstanceoftheflask_restful.Apiclassthatrepresentsthemainentrypointfortheapplication.Wepassthepreviouslycreatedflask.Blueprintinstancenamedapi_bpasanargumenttolinktheApitotheBlueprint.
TheMessageResourceclassisasubclassofflask_restful.ResourceanddeclaresthefollowingthreemethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:Thismethodreceivestheidofthemessagethathastoberetrievedintheidargument.ThecodecallstheMessage.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnomessagewiththerequestedidintheunderlyingdatabase.Incasethemessageexists,thecodecallsthemessage_schema.dumpmethodwiththeretrievedmessageasanargumenttousetheMessageSchemainstancetoserializetheMessageinstancewhoseidmatchesthespecifiedid.ThedumpmethodtakestheMessageinstanceandappliesthefieldfilteringandoutputformattingspecifiedintheMessageSchemaclass.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessageinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.delete:Thismethodreceivestheidofthemessagethathastobedeletedintheidargument.ThecodecallstheMessage.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnomessagewiththerequestedidintheunderlyingdatabase.Incasethemessageexists,thecodecallsthemessage.deletemethodwiththeretrievedmessageasanargumenttousetheMessageinstancetoeraseitselffromthedatabase.Then,thecodereturnsanemptyresponsebodyanda204NoContentstatuscode.patch:Thismethodreceivestheidofthemessagethathastobeupdatedorpatchedinthe
idargument.ThecodecallstheMessage.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnomessagewiththerequestedidintheunderlyingdatabase.Incasethemessageexists,thecodecallstherequest.get_jsonmethodtoretrievethekey-valuepairsreceivedasargumentswiththerequest.Thecodeupdatesspecificattributesincasetheyhavenewvaluesinthemessage_dictdictionaryintheMessageinstance:message.Then,thecodecallsthemessage_schema.dumpmethodtoretrieveanyerrorsgeneratedwhenserializingtheupdatedmessage.Incasetherewereerrors,thecodereturnstheerrorsandanHTTP400BadRequeststatus.Iftheserializationdidn'tgenerateerrors,thecodecallsthemessage_schema.validatemethodtoretrieveanyerrorsgeneratedwhilevalidatingtheupdatedmessage.Incasetherewerevalidationerrors,thecodereturnsthevalidationerrorsandanHTTP400BadRequeststatus.Ifthevalidationissuccessful,thecodecallstheupdatemethodfortheMessageinstancetopersistthechangesinthedatabaseandreturnstheresultsofcallingthepreviouslyexplainedself.getmethodwiththeidoftheupdatedmessageasanargument.Thisway,themethodreturnstheserializedupdatedmessageinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.
Now,wewillcreateaMessageListResourceclassthatwewillusetorepresentthecollectionofmessages.Openthepreviouslycreatedapi/views.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
classMessageListResource(Resource):
defget(self):
messages=Message.query.all()
result=message_schema.dump(messages,many=True).data
returnresult
defpost(self):
request_dict=request.get_json()
ifnotrequest_dict:
response={'message':'Noinputdataprovided'}
returnresponse,status.HTTP_400_BAD_REQUEST
errors=message_schema.validate(request_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
try:
category_name=request_dict['category']['name']
category=Category.query.filter_by(name=category_name).first()
ifcategoryisNone:
#CreateanewCategory
category=Category(name=category_name)
db.session.add(category)
#Nowthatwearesurewehaveacategory
#createanewMessage
message=Message(
message=request_dict['message'],
duration=request_dict['duration'],
category=category)
message.add(message)
query=Message.query.get(message.id)
result=message_schema.dump(query).data
returnresult,status.HTTP_201_CREATED
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_400_BAD_REQUEST
TheMessageListResourceclassisasubclassofflask_restful.ResourceanddeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:ThismethodreturnsalistwithalltheMessageinstancessavedinthedatabase.First,thecodecallstheMessage.query.allmethodtoretrievealltheMessageinstancespersistedinthedatabase.Then,thecodecallsthemessage_schema.dumpmethodwiththeretrievedmessagesandthemanyargumentsettoTruetoserializetheiterablecollectionofobjects.ThedumpmethodwilltakeeachMessageinstanceretrievedfromthedatabaseandapplythefieldfilteringandoutputformattingspecifiedtheMessageSchemaclass.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessagesinJSONformatasthebodywiththedefaultHTTP200OKstatuscode.post:Thismethodretrievesthekey-valuepairsreceivedintheJSONbody,createsanewMessageinstanceandpersistsitinthedatabase.Incasethespecifiedcategorynameexists,itusestheexistingcategory.Otherwise,themethodcreatesanewCategoryinstanceandassociatesthenewmessagetothisnewcategory.First,thecodecallstherequest.get_jsonmethodtoretrievethekey-valuepairsreceivedasargumentswiththerequest.Then,thecodecallsthemessage_schema.validatemethodtovalidatethenewmessagebuiltwiththeretrievedkey-valuepairs.RememberthattheMessageSchemaclasswillexecutethepreviouslyexplainedprocess_categorymethodbeforewecallthevalidatemethod,andtherefore,thedatawillbeprocessedbeforethevalidationtakesplace.Incasetherewerevalidationerrors,thecodereturnsthevalidationerrorsandanHTTP400BadRequeststatus.Ifthevalidationissuccessful,thecoderetrievesthecategorynamereceivedintheJSONbody,specificallyinthevalueforthe'name'keyofthe'category'key.Then,thecodecallstheCategory.query.filter_bymethodtoretrieveacategorythatmatchestheretrievedcategoryname.Ifnomatchisfound,thecodecreatesanewCategorywiththeretrievednameandpersistsinthedatabase.Then,thecodecreatesanewmessagewiththemessage,duration,andtheappropriateCategoryinstance,andpersistsitinthedatabase.Finally,thecodereturnstheserializedsavedmessageinJSONformatasthebody,withtheHTTP201Createdstatuscode.
Now,wewillcreateaCategoryResourceclassthatwewillusetorepresentacategoryresource.Openthepreviouslycreatedapi/views.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
classCategoryResource(Resource):
defget(self,id):
category=Category.query.get_or_404(id)
result=category_schema.dump(category).data
returnresult
defpatch(self,id):
category=Category.query.get_or_404(id)
category_dict=request.get_json()
ifnotcategory_dict:
resp={'message':'Noinputdataprovided'}
returnresp,status.HTTP_400_BAD_REQUEST
errors=category_schema.validate(category_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
try:
if'name'incategory_dict:
category.name=category_dict['name']
category.update()
returnself.get(id)
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_400_BAD_REQUEST
defdelete(self,id):
category=Category.query.get_or_404(id)
try:
category.delete(category)
response=make_response()
returnresponse,status.HTTP_204_NO_CONTENT
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_401_UNAUTHORIZED
TheCategoryResourceclassisasubclassofflask_restful.ResourceanddeclaresthefollowingthreemethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:Thismethodreceivestheidofthecategorythathastoberetrievedintheidargument.ThecodecallstheCategory.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnocategorywiththerequestedidintheunderlyingdatabase.Incasethemessageexists,thecodecallsthecategory_schema.dumpmethodwiththeretrievedcategoryasanargumenttousetheCategorySchemainstancetoserializetheCategoryinstancewhoseidmatchesthespecifiedid.ThedumpmethodtakestheCategoryinstanceandappliesthefieldfilteringandoutputformattingspecifiedintheCategorySchemaclass.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessageinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.patch:Thismethodreceivestheidofthecategorythathastobeupdatedorpatchedintheidargument.ThecodecallstheCategory.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnocategorywiththerequestedidintheunderlyingdatabase.Incasethecategoryexists,thecodecallstherequest.get_jsonmethodtoretrievethekey-valuepairsreceivedasargumentswiththerequest.Thecodeupdatesjustthenameattributeincaseithasanewvalueinthecategory_dictdictionaryintheCategoryinstance:category.Then,thecodecallsthecategory_schema.validatemethod
toretrieveanyerrorsgeneratedwhenvalidatingtheupdatedcategory.Incasetherewerevalidationerrors,thecodereturnsthevalidationerrorsandanHTTP400BadRequeststatus.Ifthevalidationissuccessful,thecodecallstheupdatemethodfortheCategoryinstancetopersistthechangesinthedatabaseandreturnstheresultsofcallingthepreviouslyexplainedself.getmethodwiththeidoftheupdatedcategoryasanargument.Thisway,themethodreturnstheserializedupdatedmessageinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.delete:Thismethodreceivestheidofthecategorythathastobedeletedintheidargument.ThecodecallstheCategory.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnocategorywiththerequestedidintheunderlyingdatabase.Incasethecategoryexists,thecodecallsthecategory.deletemethodwiththeretrievedcategoryasanargumenttousetheCategoryinstancetoeraseitselffromthedatabase.Then,thecodereturnsanemptyresponsebodyanda204NoContentstatuscode.
Now,wewillcreateaCategoryListResourceclassthatwewillusetorepresentthecollectionofcategories.Openthepreviouslycreatedapi/views.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
classCategoryListResource(Resource):
defget(self):
categories=Category.query.all()
results=category_schema.dump(categories,many=True).data
returnresults
defpost(self):
request_dict=request.get_json()
ifnotrequest_dict:
resp={'message':'Noinputdataprovided'}
returnresp,status.HTTP_400_BAD_REQUEST
errors=category_schema.validate(request_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
try:
category=Category(request_dict['name'])
category.add(category)
query=Category.query.get(category.id)
result=category_schema.dump(query).data
returnresult,status.HTTP_201_CREATED
exceptSQLAlchemyErrorase:
db.session.rollback()
resp=jsonify({"error":str(e)})
returnresp,status.HTTP_400_BAD_REQUEST
TheCategoryListResourceclassisasubclassofflask_restful.ResourceanddeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:ThismethodreturnsalistwithalltheCategoryinstancessavedinthedatabase.First,thecodecallstheCategory.query.allmethodtoretrievealltheCategoryinstances
persistedinthedatabase.Then,thecodecallsthecategory_schema.dumpmethodwiththeretrievedmessagesandthemanyargumentsettoTruetoserializetheiterablecollectionofobjects.ThedumpmethodwilltakeeachCategoryinstanceretrievedfromthedatabaseandapplythefieldfilteringandoutputformattingspecifiedtheCategorySchemaclass.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessagesinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.post:Thismethodretrievesthekey-valuepairsreceivedintheJSONbody,createsanewCategoryinstanceandpersistsitinthedatabase.First,thecodecallstherequest.get_jsonmethodtoretrievethekey-valuepairsreceivedasargumentswiththerequest.Then,thecodecallsthecategory_schema.validatemethodtovalidatethenewcategorybuiltwiththeretrievedkey-valuepairs.Incasetherewerevalidationerrors,thecodereturnsthevalidationerrorsandanHTTP400BadRequeststatus.Ifthevalidationissuccessful,thecodecreatesanewcategorywiththespecifiedname,andpersistsitinthedatabase.Finally,thecodereturnstheserializedsavedcategoryinJSONformatasthebody,withtheHTTP201Createdstatuscode.
ThefollowingtableshowsthemethodofourpreviouslycreatedclassesthatwewanttobeexecutedforeachcombinationofHTTPverbandscope:
HTTPverb Scope Classandmethod
GET Collectionofmessages MessageListResource.get
GET Message MessageResource.get
POST Collectionofmessages MessageListResource.post
PATCH Message MessageResource.patch
DELETE Message MessageResource.delete
GET Collectionofcategories CategoryListResource.get
GET Message CategoryResource.get
POST Collectionofmessages CategoryListResource.post
PATCH Message CategoryResource.patch
DELETE Message CategoryResource.delete
IftherequestresultsintheinvocationofaresourcewithanunsupportedHTTPmethod,Flask-RESTfulwillreturnaresponsewiththeHTTP405MethodNotAllowedstatuscode.
WemustmakethenecessaryresourceroutingconfigurationstocalltheappropriatemethodsandpassthemallthenecessaryargumentsbydefiningURLrules.Thefollowinglinesconfiguretheresourceroutingfortheapi.Opentheapi/views.pyfilecreatedearlierandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
api.add_resource(CategoryListResource,'/categories/')
api.add_resource(CategoryResource,'/categories/<int:id>')
api.add_resource(MessageListResource,'/messages/')
api.add_resource(MessageResource,'/messages/<int:id>')
Eachcalltotheapi.add_resourcemethodroutesaURLtoaresource,specificallytooneofthepreviouslydeclaredsubclassesoftheflask_restful.Resourceclass.WhenthereisarequesttotheAPIandtheURLmatchesoneoftheURLsspecifiedintheapi.add_resourcemethod,FlaskwillcallthemethodthatmatchestheHTTPverbintherequestforthespecifiedclass.
RegisteringtheblueprintandrunningmigrationsCreateanewapp.pyfilewithintheapifolder.ThefollowinglinesshowthecodethatcreatesaFlaskapplication.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder.
fromflaskimportFlask
defcreate_app(config_filename):
app=Flask(__name__)
app.config.from_object(config_filename)
frommodelsimportdb
db.init_app(app)
fromviewsimportapi_bp
app.register_blueprint(api_bp,url_prefix='/api')
returnapp
Thecodeintheapi/app.pyfiledeclaresacreate_appfunctionthatreceivestheconfigurationfilenameintheconfig_filenameargument,setupsaFlaskappwiththisconfigurationfile,andreturnstheappobject.First,thefunctioncreatesthemainentrypointfortheFlaskapplicationnamedapp.Then,thecodecallstheapp.config.from_objectmethodwiththeconfig_filenamereceivedasanargument.Thisway,theFlaskappusesthevaluesthatarespecifiedinthevariablesdefinedinthePythonmodulereceivedasanargumenttosetupthesettingsfortheFlaskapp.
Thenextlinecallstheinit_appmethodfortheflask_sqlalchemy.SQLAlchemyinstancecreatedinthemodelsmodulenameddb.ThecodepassesappasanargumenttolinkthecreatedFlaskappwiththeSQLAlchemyinstance.
Thenextlinecallstheapp.register_blueprintmethodtoregistertheblueprintcreatedintheviewsmodule,namedapi_bp.Theurl_prefixargumentissetto'/api'becausewewanttheresourcestobeavailablewith/apiasaprefix.Nowhttp://localhost:5000/api/isgoingtobetheURLfortheAPIrunningontheFlaskdevelopmentserver.Finally,thefunctionreturnstheappobject.
Createanewrun.pyfilewithintheapifolder.Thefollowinglinesshowthecodethatusesthepreviouslydefinedcreate_appfunctiontocreateaFlaskapplicationandrunit.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder.
fromappimportcreate_app
app=create_app('config')
if__name__=='__main__':
app.run(host=app.config['HOST'],
port=app.config['PORT'],
debug=app.config['DEBUG'])
Thecodeintheapi/run.pyfilecallsthecreate_appfunction,declaredintheappmodule,with'config'asanargument.ThefunctionwillsetupaFlaskappwiththismoduleastheconfigurationfile.
Thelastlinejustcallstheapp.runmethodtostarttheFlaskapplicationwiththehost,portanddebugvaluesreadfromtheconfigmodule.Thecodestartstheapplicationbycallingtherunmethodtoimmediatelylaunchalocalserver.Rememberthatwecouldalsoachievethesamegoalusingtheflaskcommand-linescript.
Createanewmigrate.pyfilewithintheapifolder.Thefollowinglinesshowthecodethatuseflask_scriptandflask_migratetorunmigrations.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
fromflask_scriptimportManager
fromflask_migrateimportMigrate,MigrateCommand
frommodelsimportdb
fromrunimportapp
migrate=Migrate(app,db)
manager=Manager(app)
manager.add_command('db',MigrateCommand)
if__name__=='__main__':
manager.run()
Thecodecreatesaninstanceofflask_migrate.MigratewiththeFlaskappcreatedinthepreviouslyexplainedrunmodule,app,andtheflask_sqlalchemy.SQLAlchemyinstancecreatedinthemodelsmodule,db.Then,thecodecreatesaflask_script.ManagerclasswiththeFlaskappasanargumentandsavesitsreferenceinthemanagervariable.Thenextlinecallstheadd_commandmethodwith'db'andMigrateCommandasarguments.ThemainfunctioncallstherunmethodfortheManagerinstance.
Thisway,aftertheextensioninitializes,thecodeaddsadbgrouptothecommand-lineoptions.Thedbgrouphasmanysub-commandsthatwewillusethroughthemigrate.pyscript.
Now,wewillrunthescriptstorunmigrationsandgeneratethenecessarytablesinthePostgreSQLdatabase.MakesureyourunthescriptsintheterminalorCommandPromptwindowinwhichyouhaveactivatedthevirtualenvironmentandthatyouarelocatedinthe
apifolder.
Runthefirstscript,thatinitializesmigrationsupportfortheapplication.
pythonmigrate.pydbinit
Thefollowinglinesshowthesampleoutputgeneratedafterrunningthepreviousscript.Youroutputwillbedifferentaccordingtothebasefolderinwhichyouhavecreatedthevirtualenvironment:
Creatingdirectory/Users/gaston/PythonREST/Flask02/api/migrations...done
Creatingdirectory/Users/gaston/PythonREST/Flask02/api/migrations/versions
...done
Generating/Users/gaston/PythonREST/Flask02/api/migrations/alembic.ini...
done
Generating/Users/gaston/PythonREST/Flask02/api/migrations/env.py...done
Generating/Users/gaston/PythonREST/Flask02/api/migrations/README...done
Generating/Users/gaston/PythonREST/Flask02/api/migrations/script.py.mako...
done
Pleaseeditconfiguration/connection/loggingsettingsin
'/Users/gaston/PythonREST/Flask02/api/migrations/alembic.ini'before
proceeding.
Thescriptgeneratedanewmigrationssub-folderwithintheapifolderwithaversionssub-folderandmanyotherfiles.
Runthesecondscriptthatpopulatesthemigrationscriptwiththedetectedchangesinthemodels.Inthiscase,itisthefirsttimewepopulatethemigrationscript,andtherefore,themigrationscriptwillgeneratethetablesthatwillpersistourtwomodels:CategoryandMessage:
pythonmigrate.pydbmigrate
Thefollowinglinesshowthesampleoutputgeneratedafterrunningthepreviousscript.Youroutputwillbedifferentaccordingtothebasefolderinwhichyouhavecreatedthevirtualenvironment:
INFO[alembic.runtime.migration]ContextimplPostgresqlImpl.
INFO[alembic.runtime.migration]WillassumetransactionalDDL.
INFO[alembic.autogenerate.compare]Detectedaddedtable'category'
INFO[alembic.autogenerate.compare]Detectedaddedtable'message'
Generating
/Users/gaston/PythonREST/Flask02/api/migrations/versions/417543056ac3_.py...
done
Theoutputindicatesthattheapi/migrations/versions/417543056ac3_.pyfileincludesthecodetocreatethecategoryandmessagetables.Thefollowinglinesshowthecodeforthisfilethatwasautomaticallygeneratedbasedonthemodels.Notethatthefilenamewillbedifferentinyourconfiguration.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
"""emptymessage
RevisionID:417543056ac3
Revises:None
CreateDate:2016-08-0801:05:31.134631
"""
#revisionidentifiers,usedbyAlembic.
revision='417543056ac3'
down_revision=None
fromalembicimportop
importsqlalchemyassa
defupgrade():
###commandsautogeneratedbyAlembic-pleaseadjust!###
op.create_table('category',
sa.Column('id',sa.Integer(),nullable=False),
sa.Column('name',sa.String(length=150),nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('message',
sa.Column('id',sa.Integer(),nullable=False),
sa.Column('message',sa.String(length=250),nullable=False),
sa.Column('duration',sa.Integer(),nullable=False),
sa.Column('creation_date',sa.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),nullable=False),
sa.Column('category_id',sa.Integer(),nullable=False),
sa.Column('printed_times',sa.Integer(),server_default='0',
nullable=False),
sa.Column('printed_once',sa.Boolean(),server_default='false',
nullable=False),
sa.ForeignKeyConstraint(['category_id'],['category.id'],
ondelete='CASCADE'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('message')
)
###endAlembiccommands###
defdowngrade():
###commandsautogeneratedbyAlembic-pleaseadjust!###
op.drop_table('message')
op.drop_table('category')
###endAlembiccommands###
Thecodedefinestwofunctions:upgradeanddowngrade.Theupgradefunctionrunsthenecessarycodetocreatethecategoryandmessagetablesbymakingcallstoalembic.op.create_table.Thedowngradefunctionrunsthenecessarycodetogobacktothepreviousversion.
Runthethirdscripttoupgradethedatabase:
pythonmigrate.pydbupgrade
Thefollowinglinesshowthesampleoutputgeneratedafterrunningthepreviousscript:
INFO[alembic.runtime.migration]ContextimplPostgresqlImpl.
INFO[alembic.runtime.migration]WillassumetransactionalDDL.
INFO[alembic.runtime.migration]Runningupgrade->417543056ac3,empty
message
Thepreviousscriptcalledtheupgradefunctiondefinedintheautomaticallygeneratedapi/migrations/versions/417543056ac3_.pyscript.Don'tforgetthatthefilenamewillbedifferentinyourconfiguration.
Afterwerunthepreviousscripts,wecanusethePostgreSQLcommandlineoranyotherapplicationthatallowsustoeasilyverifythecontentsofthePostreSQLdatabasetocheckthetablesthatthemigrationgenerated.
Runthefollowingcommandtolistthegeneratedtables.Incasethedatabasenameyouareusingisnotnamedmessages,makesureyouusetheappropriatedatabasename.
psql--username=user_name--dbname=messages--command="\dt"
Thefollowinglinesshowtheoutputwithallthegeneratedtablenames:
Listofrelations
Schema|Name|Type|Owner
--------+-----------------+-------+-----------
public|alembic_version|table|user_name
public|category|table|user_name
public|message|table|user_name
(3rows)
SQLAlchemygeneratedthetables,theuniqueconstraints,andtheforeignkeysbasedontheinformationincludedinourmodels.
category:PersiststheCategorymodel.message:PersiststheMessagemodel.
ThefollowingcommandwillallowyoutocheckthecontentsofthefourtablesafterwecomposeandsendHTTPrequeststotheRESTfulAPIandmakeCRUDoperationstothetwotables.ThecommandsassumethatyouarerunningPostgreSQLonthesamecomputerinwhichyouarerunningthecommand:
psql--username=user_name--dbname=messages--command="SELECT*FROM
category;"
psql--username=user_name--dbname=messages--command="SELECT*FROMmessage;"
Tip
InsteadofworkingwiththePostgreSQLcommand-lineutility,youcanuseaGUItooltocheckthecontentsofthePostgreSQLdatabase.Youalsousealsothedatabasetoolsincluded
inyourfavoriteIDEtocheckthecontentsfortheSQLitedatabase.
Alembicgeneratedanadditionaltablenamedalembic_versionthatsavestheversionnumberforthedatabaseintheversion_numcolumn.Thistablemakesispossibleforthemigrationscriptstoretrievethecurrentversionofthedatabaseandupgradeordowngradeitbasedonourneeds.
CreatingandretrievingrelatedresourcesNow,wecanruntheapi/run.pyscriptthatlaunchesFlask'sdevelopment.Executethefollowingcommandintheapifolder.
pythonrun.py
Thefollowinglinesshowtheoutputafterweexecutetheprecedingcommand.Thedevelopmentserverislisteningatport5000.
*Runningonhttp://127.0.0.1:5000/(PressCTRL+Ctoquit)
*Restartingwithstat
*Debuggerisactive!
*Debuggerpincode:198-040-402
Now,wewillusetheHTTPiecommandoritscurlequivalentstocomposeandsendHTTPrequeststotheAPI.WewilluseJSONfortherequeststhatrequireadditionaldata.RememberthatyoucanperformthesametaskswithyourfavoriteGUI-basedtool.
First,wewillcomposeandsendHTTPrequeststocreatetwomessagecategories:
httpPOST:5000/api/categories/name='Information'
httpPOST:5000/api/categories/name='Warning'
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Information"}'
:5000/api/categories/
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Warning"}'
:5000/api/categories/
TheprecedingcommandswillcomposeandsendtwoPOSTHTTPrequestswiththespecifiedJSONkey-valuepair.Therequestsspecify/api/categories/,andtherefore,theywillmatchthe'/api'url_prefixfortheapi_bpblueprint.Then,therequestwillmatchthe'/categories/'URLroutefortheCategoryListresourceandruntheCategoryList.postmethod.Themethoddoesn'treceiveargumentsbecausetheURLroutedoesn'tincludeanyparameters.AstheHTTPverbfortherequestisPOST,Flaskcallsthepostmethod.IfthetwonewCategoryinstancesweresuccessfullypersistedinthedatabase,thetwocallswillreturnanHTTP201CreatedstatuscodeandtherecentlypersistedCategoryserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponseforthetwoHTTPrequests,withthenewCategoryobjectsintheJSONresponses.
NotethattheresponsesincludetheURL,url,forthecreatedcategories.Themessagesarrayisemptyinbothcasesbecausetherearen'tmessagesrelatedtoeachnewcategoryyet:
HTTP/1.0201CREATED
Content-Length:116
Content-Type:application/json
Date:Mon,08Aug201605:26:58GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"id":1,
"messages":[],
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
}
HTTP/1.0201CREATED
Content-Length:112
Content-Type:application/json
Date:Mon,08Aug201605:27:05GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"id":2,
"messages":[],
"name":"Warning",
"url":"http://localhost:5000/api/categories/2"
}
Now,wewillcomposeandsendHTTPrequeststocreatetwomessagesthatbelongtothefirstmessagecategorywerecentlycreated:Information.Wewillspecifythecategorykeywiththenameofthedesiredmessagecategory.ThedatabasetablethatpersiststheMessagemodelwillsavethevalueoftheprimarykeyoftherelatedCategorywhosenamevaluematchestheoneweprovide:
httpPOST:5000/api/messages/message='Checkingtemperaturesensor'duration=5
category="Information"
httpPOST:5000/api/messages/message='Checkinglightsensor'duration=8
category="Information"
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Checking
temperaturesensor","category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Checking
lightsensor","category":"Information"}':5000/api/messages/
ThefirstcommandwillcomposeandsendthefollowingHTTPrequest:POSThttp://localhost:5000/api/messages/withthefollowingJSONkey-valuepairs:
{
"message":"Checkingtemperaturesensor",
"category":"Information"
}
ThesecondcommandwillcomposeandsendthesameHTTPrequestwiththefollowingJSONkey-valuepairs:
{
"message":"Checkinglightsensor",
"category":"Information"
}
Therequestsspecify/api/categories/,andtherefore,theywillmatchthe'/api'url_prefixfortheapi_bpblueprint.Then,therequestwillmatchthe'/messages/'URLroutefortheMessageListresourceandruntheMessageList.postmethod.Themethoddoesn'treceiveargumentsbecausetheURLroutedoesn'tincludeanyparameters.AstheHTTPverbfortherequestisPOST,Flaskcallsthepostmethod.ThetheMessageSchema.process_categorymethodwillprocessthedataforthecategoryandtheMessageListResource.postmethodwillretrievetheCategorythatmatchesthespecifiedcategorynamefromthedatabase,touseitastherelatedcategoryforthenewmessage.IfthetwonewMessageinstancesweresuccessfullypersistedinthedatabase,thetwocallswillreturnanHTTP201CreatedstatuscodeandtherecentlypersistedMessageserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponseforthetwoHTTPrequests,withthenewMessageobjectsintheJSONresponses.NotethattheresponsesincludetheURL,url,forthecreatedmessages.Inaddition,theresponseincludestheid,name,andurlfortherelatedcategory.
HTTP/1.0201CREATED
Content-Length:369
Content-Type:application/json
Date:Mon,08Aug201615:18:43GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:18:43.260474+00:00",
"duration":5,
"id":1,
"message":"Checkingtemperaturesensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/1"
}
HTTP/1.0201CREATED
Content-Length:363
Content-Type:application/json
Date:Mon,08Aug201615:27:30GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:27:30.124511+00:00",
"duration":8,
"id":2,
"message":"Checkinglightsensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/2"
}
WecanruntheprecedingcommandstocheckthecontentsofthetablesthatthemigrationscreatedinthePostgreSQLdatabase.Wewillnoticethatthecategory_idcolumnforthemessagetablesavesthevalueoftheprimarykeyoftherelatedrowinthecategorytable.TheMessageSchemaclassusesafields.Nestedinstancetorendertheid,urlandnamefieldsfortherelatedCategory.ThefollowingscreenshotshowsthecontentsforthecategoryandthemessagetableinaPostgreSQLdatabaseafterrunningtheHTTPrequests:
Now,wewillcomposeandsendanHTTPrequesttoretrievethecategorythatcontainstwomessages,thatisthecategoryresourcewhoseidorprimarykeyisequalto1.Don'tforgettoreplace1withtheprimarykeyvalueofthecategorywhosenameisequalto'Information'inyourconfiguration:
http:5000/api/categories/1
Thefollowingistheequivalentcurlcommand:
curl-iXGET:5000/api/categories/1
TheprecedingcommandwillcomposeandsendaGETHTTPrequest.Therequesthasanumberafter/api/categories/,andtherefore,itwillmatch'/categories/<int:id>'andruntheCategoryResource.getmethod,thatis,thegetmethodfortheCategoryResourceclass.IfaCategoryinstancewiththespecifiedidexistsinthedatabase,thecalltothemethodwillwillreturnanHTTP200OKstatuscodeandtheCategoryinstanceserializedtoJSONintheresponsebody.TheCategorySchemaclassusesafields.Nestedinstancetorenderallthefieldsforallthemessagesrelatedtothecategoryexceptingthecategoryfield.Thefollowinglinesshowasampleresponse:
HTTP/1.0200OK
Content-Length:1078
Content-Type:application/json
Date:Mon,08Aug201616:09:10GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"id":1,
"messages":[
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:27:30.124511+00:00",
"duration":8,
"id":2,
"message":"Checkinglightsensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/2"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:18:43.260474+00:00",
"duration":5,
"id":1,
"message":"Checkingtemperaturesensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/1"
}
],
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
}
Now,wewillcomposeandsendaPOSTHTTPrequesttocreateamessagerelatedtoacategorynamethatdoesn'texist:'Error':
httpPOST:5000/api/messages/message='Temperaturesensorerror'duration=10
category="Error"
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"
Temperaturesensorerror","category":"Error"}':5000/api/messages/
TheCategoryListResource.postmethodwon'tbeabletoretrieveaCategoryinstancewhosenameisequaltothespecifiedvalue,andtherefore,themethodwillcreateanewCategory,saveitanduseitastherelatedcategoryforthenewmessage.ThefollowinglinesshowanexampleresponsefortheHTTPrequest,withthenewMessageobjectintheJSONresponsesandthedetailsforthenewCategoryobjectrelatedtothemessage:
HTTP/1.0201CREATED
Content-Length:361
Content-Type:application/json
Date:Mon,08Aug201617:20:22GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"category":{
"id":3,
"name":"Error",
"url":"http://localhost:5000/api/categories/3"
},
"creation_date":"2016-08-08T14:20:22.103752+00:00",
"duration":10,
"id":3,
"message":"Temperaturesensorerror",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/3"
}
WecanrunthecommandsexplainedearliertocheckthecontentsofthetablesthatthemigrationscreatedinthePostgreSQLdatabase.Wewillnoticethatwehaveanewrowinthecategorytablewiththerecentlyaddedcategorywhenwecreatedanewmessage.ThefollowingscreenshotshowsthecontentsforthecategoryandmessagetablesinaPostgreSQLdatabaseafterrunningtheHTTPrequests:
Testyourknowledge1. Marshmallowis:
1. AlightweightlibraryforconvertingcomplexdatatypestoandfromnativePythondatatypes.
2. AnORM.3. AlightweightwebframeworkthatreplacesFlask.
2. SQLAlchemyis:1. AlightweightlibraryforconvertingcomplexdatatypestoandfromnativePython
datatypes.2. AnORM.3. AlightweightwebframeworkthatreplacesFlask.
3. Themarshmallow.pre_loaddecorator:1. RegistersamethodtorunafteranyinstanceoftheMessageSchemaclassiscreated.2. Registersamethodtoinvokeafterserializinganobject.3. Registersamethodtoinvokebeforedeserializinganobject.
4. ThedumpmethodforanyinstanceofaSchemasubclass:1. RoutesURLstoPythonprimitives.2. Persiststheinstanceorcollectionofinstancespassedasanargumenttothedatabase.3. Takestheinstanceorcollectionofinstancespassedasanargumentandappliesthe
fieldfilteringandoutputformattingspecifiedintheSchemasubclasstotheinstanceorcollectionofinstances.
5. Whenwedeclareanattributeasaninstanceofthemarshmallow.fields.Nestedclass:1. ThefieldwillnestasingleSchemaoracollectionofSchemabasedonthevaluefor
themanyargument.2. ThefieldwillnestasingleSchema.IfwewanttonestacollectionofSchema,wehave
touseaninstanceofthemarshmallow.fields.NestedCollectionclass.3. ThefieldwillnestacollectionofSchema.IfwewanttonestasingleSchema,wehave
touseaninstanceofthemarshmallow.fields.NestedSingleclass.
SummaryInthischapter,weexpandedthecapabilitiesofthepreviousversionoftheRESTfulAPIthatwecreatedinthepreviouschapter.WeusedSQLAlchemyasourORMtoworkwithaPostgreSQLdatabase.Weinstalledmanypackagestosimplifymanycommontasks,wrotecodeforthemodelsandtheirrelationships,andworkedwithschemastovalidate,serialize,anddeserializethesemodels.
Wecombinedblueprintswithresourcefulroutingandwereabletogeneratethedatabasefromthemodels.WecomposedandsentmanyHTTPrequeststoourRESTfulAPIandanalyzedhoweachHTTPrequestwasprocessedinourcodeandhowthemodelspersistedinthedatabasetables.
NowthatwebuiltacomplexAPIwithFlask,Flask-RESTful,andSQLAlchemy,wewilluseadditionalfeaturesandaddsecurityandauthentication,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter7.ImprovingandAddingAuthenticationtoanAPIwithFlaskInthischapter,wewillimprovetheRESTfulAPIthatwestartedinthepreviouschapterandwewilladdauthenticationrelatedsecuritytoit.Wewill:
ImproveuniqueconstraintsinthemodelsUpdatefieldsforaresourcewiththePATCHmethodCodeagenericpaginationclassAddpaginationfeaturestotheAPIUnderstandthestepstoaddauthenticationandpermissionsAddausermodelCreateaschematovalidate,serializeanddeserializeusersAddauthenticationtoresourcesCreateresourceclassestohandleusersRunmigrationstogeneratetheusertableComposerequestswiththenecessaryauthentication
ImprovinguniqueconstraintsinthemodelsWhenwecreatedtheCategorymodel,wespecifiedtheTruevaluefortheuniqueargumentwhenwecreatedthedb.Columninstancenamedname.Asaresult,themigrationsgeneratedthenecessaryuniqueconstrainttomakesurethatthenamefieldhasuniquevaluesinthecategorytable.Thisway,thedatabasewon'tallowustoinsertduplicatevaluesforcategory.name.However,theerrormessagegeneratedwhenwetrytodosoisnotclear.
Runthefollowingcommandtocreateacategorywithaduplicatename.Thereisalreadyanexistingcategorywiththenameequalto'Information':
httpPOST:5000/api/categories/name='Information'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Information"}'
:5000/api/categories/
ThepreviouscommandwillcomposeandsendaPOSTHTTPrequestwiththespecifiedJSONkey-valuepair.Theuniqueconstraintinthecategory.namefieldwon'tallowthedatabasetabletopersistthenewcategory.Thus,therequestwillreturnanHTTP400BadRequeststatuscodewithanintegrityerrormessage.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BADREQUEST
Content-Length:282
Content-Type:application/json
Date:Mon,15Aug201603:53:27GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"error":"(psycopg2.IntegrityError)duplicatekeyvalueviolatesunique
constraint"category_name_key"\nDETAIL:Key(name)=(Information)
alreadyexists.\n[SQL:'INSERTINTOcategory(name)VALUES(%
(name)s)
RETURNINGcategory.id'][parameters:{'name':'Information'}]"
}
Obviously,theerrormessageisextremelytechnicalandprovidestoomanydetailsaboutthedatabaseandthequerythatfailed.Wemightparsetheerrormessagetoautomaticallygenerateamoreuserfriendlyerrormessage.However,insteadofdoingso,wewanttoavoidtryingtoinsertarowthatweknowwillfail.Wewilladdcodetomakesurethatacategoryisuniquebeforewetrytopersistit.Ofcourse,thereisstillachancetoreceivethepreviouslyshownerrorifsomebodyinsertsacategorywiththesamenamebetweenthetimewerunourcode,indicatingthatacategorynameisunique,andpersistthechangesinthedatabase.However,thechancesarelowerandwecanreducethechangesofthepreviouslyshownerrormessagetobeshown.
Tip
Inaproduction-readyRESTAPIweshouldneverreturntheerrormessagesreturnedbySQLAlchemyoranyotherdatabase-relateddata,asitmightincludesensitivedatathatwedon'twanttheusersofourAPItobeabletoretrieve.Inthiscase,wearereturningalltheerrorsfordebuggingpurposesandtobeabletoimproveourAPI.
Now,wewilladdanewclassmethodtotheCategoryclasstoallowustodeterminewhetheranameisuniqueornot.Opentheapi/models.pyfileandaddthefollowinglineswithinthedeclarationoftheCategoryclass.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
@classmethod
defis_unique(cls,id,name):
existing_category=cls.query.filter_by(name=name).first()
ifexisting_categoryisNone:
returnTrue
else:
ifexisting_category.id==id:
returnTrue
else:
returnFalse
ThenewCategory.is_uniqueclassmethodreceivestheidandthenameforthecategorythatwewanttomakesurethathasauniquename.Ifthecategoryisanewonethathasn'tbeensavedyet,wewillreceivea0fortheidvalue.Otherwise,wewillreceivethecategoryidintheargument.
Themethodcallsthequery.filter_bymethodforthecurrentclasstoretrieveacategorywhosenamematchestheothercategoryname.Incasethereisacategorythatmatchesthecriteria,themethodwillreturnTrueonlyiftheidisthesameonethantheonereceivedintheargument.Incasenocategorymatchesthecriteria,themethodwillreturnTrue.
WewillusethepreviouslycreatedclassmethodtocheckwhetheracategoryisuniqueornotbeforecreatingandpersistingitintheCategoryListResource.postmethod.Opentheapi/views.pyfileandreplacetheexistingpostmethoddeclaredintheCategoryListResourceclasswiththefollowinglines.Thelinesthathavebeenaddedormodifiedarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
defpost(self):
request_dict=request.get_json()
ifnotrequest_dict:
resp={'message':'Noinputdataprovided'}
returnresp,status.HTTP_400_BAD_REQUEST
errors=category_schema.validate(request_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
category_name=request_dict['name']
ifnotCategory.is_unique(id=0,name=category_name):
response={'error':'Acategorywiththesamenamealready
exists'}
returnresponse,status.HTTP_400_BAD_REQUEST
try:
category=Category(category_name)
category.add(category)
query=Category.query.get(category.id)
result=category_schema.dump(query).data
returnresult,status.HTTP_201_CREATED
exceptSQLAlchemyErrorase:
db.session.rollback()
resp={"error":str(e)}
returnresp,status.HTTP_400_BAD_REQUEST
Now,wewillperformthesamevalidationintheCategoryResource.patchmethod.Opentheapi/views.pyfileandreplacetheexistingpatchmethoddeclaredintheCategoryResourceclasswiththefollowinglines.Thelinesthathavebeenaddedormodifiedarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
defpatch(self,id):
category=Category.query.get_or_404(id)
category_dict=request.get_json()
ifnotcategory_dict:
resp={'message':'Noinputdataprovided'}
returnresp,status.HTTP_400_BAD_REQUEST
errors=category_schema.validate(category_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
try:
if'name'incategory_dict:
category_name=category_dict['name']
ifCategory.is_unique(id=id,name=category_name):
category.name=category_name
else:
response={'error':'Acategorywiththesamename
already
exists'}
returnresponse,status.HTTP_400_BAD_REQUEST
category.update()
returnself.get(id)
exceptSQLAlchemyErrorase:
db.session.rollback()
resp={"error":str(e)}
returnresp,status.HTTP_400_BAD_REQUEST
Runthefollowingcommandtoagaincreateacategorywithaduplicatename:
httpPOST:5000/api/categories/name='Information'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"Information"}'
:5000/api/categories/
ThepreviouscommandwillcomposeandsendaPOSTHTTPrequestwiththespecifiedJSONkey-valuepair.Thechangeswemadewillgeneratearesponsewithauserfriendlyerrormessageandwillavoidtryingtopersistthechanges.TherequestwillreturnanHTTP400
BadRequeststatuscodewiththeerrormessageintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BADREQUEST
Content-Length:64
Content-Type:application/json
Date:Mon,15Aug201604:38:43GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"error":"Acategorywiththesamenamealreadyexists"
}
Now,wewilladdanewclassmethodtotheMessageclasstoallowustodeterminewhetheramessageisuniqueornot.Opentheapi/models.pyfileandaddthefollowinglineswithinthedeclarationoftheMessageclass.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
@classmethod
defis_unique(cls,id,message):
existing_message=cls.query.filter_by(message=message).first()
ifexisting_messageisNone:
returnTrue
else:
ifexisting_message.id==id:
returnTrue
else:
returnFalse
ThenewMessage.is_uniqueclassmethodreceivestheidandthemessageforthemessagethatwewanttomakesurethathasauniquevalueforthemessagefield.Ifthemessageisanewonethathasn'tbeensavedyet,wewillreceivea0fortheidvalue.Otherwise,wewillreceivethemessageidintheargument.
Themethodcallsthequery.filter_bymethodforthecurrentclasstoretrieveamessagewhosemessagefieldmatchestheothermessage'smessage.Incasethereisamessagethatmatchesthecriteria,themethodwillreturnTrueonlyiftheidisthesameonethantheonereceivedintheargument.Incasenomessagematchesthecriteria,themethodwillreturnTrue.
WewillusethepreviouslycreatedclassmethodtocheckwhetheramessageisuniqueornotbeforecreatingandpersistingitintheMessageListResource.postmethod.Opentheapi/views.pyfileandreplacetheexistingpostmethoddeclaredintheMessageListResourceclasswiththefollowinglines.Thelinesthathavebeenaddedormodifiedarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
defpost(self):
request_dict=request.get_json()
ifnotrequest_dict:
response={'message':'Noinputdataprovided'}
returnresponse,status.HTTP_400_BAD_REQUEST
errors=message_schema.validate(request_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
message_message=request_dict['message']
ifnotMessage.is_unique(id=0,message=message_message):
response={'error':'Amessagewiththesamemessagealready
exists'}
returnresponse,status.HTTP_400_BAD_REQUEST
try:
category_name=request_dict['category']['name']
category=Category.query.filter_by(name=category_name).first()
ifcategoryisNone:
#CreateanewCategory
category=Category(name=category_name)
db.session.add(category)
#Nowthatwearesurewehaveacategory
#createanewMessage
message=Message(
message=message_message,
duration=request_dict['duration'],
category=category)
message.add(message)
query=Message.query.get(message.id)
result=message_schema.dump(query).data
returnresult,status.HTTP_201_CREATED
exceptSQLAlchemyErrorase:
db.session.rollback()
resp={"error":str(e)}
returnresp,status.HTTP_400_BAD_REQUEST
Now,wewillperformthesamevalidationintheMessageResource.patchmethod.Opentheapi/views.pyfileandreplacetheexistingpatchmethoddeclaredintheMessageResourceclasswiththefollowinglines.Thelinesthathavebeenaddedormodifiedarehighlighted.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
defpatch(self,id):
message=Message.query.get_or_404(id)
message_dict=request.get_json(force=True)
if'message'inmessage_dict:
message_message=message_dict['message']
ifMessage.is_unique(id=id,message=message_message):
message.message=message_message
else:
response={'error':'Amessagewiththesamemessagealready
exists'}
returnresponse,status.HTTP_400_BAD_REQUEST
if'duration'inmessage_dict:
message.duration=message_dict['duration']
if'printed_times'inmessage_dict:
message.printed_times=message_dict['printed_times']
if'printed_once'inmessage_dict:
message.printed_once=message_dict['printed_once']
dumped_message,dump_errors=message_schema.dump(message)
ifdump_errors:
returndump_errors,status.HTTP_400_BAD_REQUEST
validate_errors=message_schema.validate(dumped_message)
ifvalidate_errors:
returnvalidate_errors,status.HTTP_400_BAD_REQUEST
try:
message.update()
returnself.get(id)
exceptSQLAlchemyErrorase:
db.session.rollback()
resp={"error":str(e)}
returnresp,status.HTTP_400_BAD_REQUEST
Runthefollowingcommandtocreateamessagewithaduplicatevalueforthemessagefield:
httpPOST:5000/api/messages/message='Checkingtemperaturesensor'
duration=25category="Information"
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Checking
temperaturesensor","duration":25,"category":"Information"}'
:5000/api/messages/
ThepreviouscommandwillcomposeandsendaPOSTHTTPrequestwiththespecifiedJSONkey-valuepair.Thechangeswemadewillgeneratearesponsewithauserfriendlyerrormessageandwillavoidtryingtopersistthechangesinthemessage.TherequestwillreturnanHTTP400BadRequeststatuscodewiththeerrormessageintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BADREQUEST
Content-Length:66
Content-Type:application/json
Date:Mon,15Aug201604:55:46GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"error":"Amessagewiththesamemessagealreadyexists"
}
UpdatingfieldsforaresourcewiththePATCHmethodAsweexplainedinChapter6,WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlask,ourAPIisabletoupdateasinglefieldforanexistingresource,andtherefore,weprovideanimplementationforthePATCHmethod.Forexample,wecanusethePATCHmethodtoupdateanexistingmessageandsetthevalueforitsprinted_onceandprinted_timesfieldstotrueand1.Wedon'twanttousethePUTmethodbecausethismethodismeanttoreplaceanentiremessage.ThePATCHmethodismeanttoapplyadeltatoanexistingmessage,andtherefore,itistheappropriatemethodtojustchangethevalueofthosetwofields.
Now,wewillcomposeandsendanHTTPrequesttoupdateanexistingmessage,specifically,toupdatethevalueoftheprinted_onceandprinted_timesfields.Becausewejustwanttoupdatetwofields,wewillusethePATCHmethodinsteadofPUT.Makesureyoureplace1withtheidorprimarykeyofanexistingmessageinyourconfiguration:
httpPATCH:5000/api/messages/1printed_once=trueprinted_times=1
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d'{"printed_once":"true",
"printed_times":1}':5000/api/messages/1
ThepreviouscommandwillcomposeandsendaPATCHHTTPrequestwiththefollowingspecifiedJSONkey-valuepairs:
{
"printed_once":true,
"printed_times":1
}
Therequesthasanumberafter/api/messages/,andtherefore,itwillmatch'/messages/<int:id>'andruntheMessageResource.patchmethod,thatis,thepatchmethodfortheMessageResourceclass.IfaMessageinstancewiththespecifiedidexists,thecodewillretrievethevaluesfortheprinted_timesandprinted_oncekeysintherequestdictionaryupdatetheMessageinstanceandvalidateit.
IftheupdatedMessageinstanceisvalid,thecodewillpersistthechangesinthedatabaseandthecalltothemethodwillreturnanHTTP200OKstatuscodeandtherecentlyupdatedMessageinstanceserializedtoJSONintheresponsebody.Thefollowinglinesshowasampleresponse:
HTTP/1.0200OK
Content-Length:368
Content-Type:application/json
Date:Tue,09Aug201622:38:39GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:18:43.260474+00:00",
"duration":5,
"id":1,
"message":"Checkingtemperaturesensor",
"printed_once":true,
"printed_times":1,
"url":"http://localhost:5000/api/messages/1"
}
WecanrunthecommandsexplainedinChapter6,WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlask,tocheckthecontentsofthetablesthatthemigrationscreatedinthePostgreSQLdatabase.Wewillnoticethattheprinted_timesandprinted_oncevalueshavebeenupdatedfortherowinthemessagetable.ThefollowingscreenshotshowsthecontentsfortheupdatedrowofthemessagetableinaPostgreSQLdatabaseafterrunningtheHTTPrequest.ThescreenshotshowstheresultsofexecutingthefollowingSQLquery:SELECT*FROMmessageWHEREid=1:
CodingagenericpaginationclassOurdatabasehasafewrowsforeachofthetablesthatpersistthemodelswehavedefined.However,afterwestartworkingwithourAPIinareal-lifeproductionenvironment,wewillhavehundredsofmessages,andtherefore,wewillhavetodealwithlargeresultsets.Thus,wewillcreateagenericpaginationclassandwewilluseittoeasilyspecifyhowwewantlargeresultssetstobesplitintoindividualpagesofdata.
First,wewillcomposeandsendHTTPrequeststocreate9messagesthatbelongtooneofthecategorieswehavecreated:Information.Thisway,wewillhaveatotalof12messagespersistedinthedatabase.Wehad3messagesandweadd9more.
httpPOST:5000/api/messages/message='Initializinglightcontroller'
duration=25category="Information"
httpPOST:5000/api/messages/message='Initializinglightsensor'duration=20
category="Information"
httpPOST:5000/api/messages/message='Checkingpressuresensor'duration=18
category="Information"
httpPOST:5000/api/messages/message='Checkinggassensor'duration=14
category="Information"
httpPOST:5000/api/messages/message='SettingADCresolution'duration=22
category="Information"
httpPOST:5000/api/messages/message='Settingsamplerate'duration=15
category="Information"
httpPOST:5000/api/messages/message='Initializingpressuresensor'
duration=18category="Information"
httpPOST:5000/api/messages/message='Initializinggassensor'duration=16
category="Information"
httpPOST:5000/api/messages/message='Initializingproximitysensor'
duration=5category="Information"
Thefollowingaretheequivalentcurlcommands:
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"
Initializinglightcontroller","duration":25,"category":"Information"}'
:5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Initializing
lightsensor","duration":20,"category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Checking
pressuresensor","duration":18,"category":"Information"}'
:5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Checkinggas
sensor","duration":14,"category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"SettingADC
resolution","duration":22,"category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Setting
samplerate","duration":15,"category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Initializing
pressuresensor","duration":18,"category":"Information"}'
:5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Initializing
gassensor","duration":16,"category":"Information"}':5000/api/messages/
curl-iXPOST-H"Content-Type:application/json"-d'{"message":"Initializing
proximitysensor","duration":5,"category":"Information"}'
:5000/api/messages/
ThepreviouscommandswillcomposeandsendninePOSTHTTPrequestswiththespecifiedJSONkey-valuepairs.Therequestspecifies/api/messages/,andtherefore,itwillmatch'/messages/'andruntheMessageListResource.postmethod,thatis,thepostmethodfortheMessageListResourceclass.
Now,wehave12messagesinourdatabase.However,wedon'twanttoretrievethe12messageswhenwecomposeandsendaGETHTTPrequestto/api/messages/.Wewillcreateacustomizablegenericpaginationclasstoincludeamaximumof5resourcesineachindividualpageofdata.
Opentheapi/config.pyfileandaddthefollowinglinesthatdeclaretwovariablesthatconfiguretheglobalpaginationsettings.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
PAGINATION_PAGE_SIZE=5
PAGINATION_PAGE_ARGUMENT_NAME='page'
ThevalueforthePAGINATION_PAGE_SIZEvariablespecifiesaglobalsettingwiththedefaultvalueforthepagesize,alsoknownaslimit.ThevalueforthePAGINATION_PAGE_ARGUMENT_NAMEspecifiesaglobalsettingwiththedefaultvaluefortheargumentnamethatwewilluseinourrequeststospecifythepagenumberwewanttoretrieve.
Createanewhelpers.pyfilewithintheapifolder.ThefollowinglinesshowthecodethatcreatesanewPaginationHelperclass.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
fromflaskimporturl_for
fromflaskimportcurrent_app
classPaginationHelper():
def__init__(self,request,query,resource_for_url,key_name,schema):
self.request=request
self.query=query
self.resource_for_url=resource_for_url
self.key_name=key_name
self.schema=schema
self.results_per_page=current_app.config['PAGINATION_PAGE_SIZE']
self.page_argument_name=
current_app.config['PAGINATION_PAGE_ARGUMENT_NAME']
defpaginate_query(self):
#Ifnopagenumberisspecified,weassumetherequestwantspage#1
page_number=self.request.args.get(self.page_argument_name,1,
type=int)
paginated_objects=self.query.paginate(
page_number,
per_page=self.results_per_page,
error_out=False)
objects=paginated_objects.items
ifpaginated_objects.has_prev:
previous_page_url=url_for(
self.resource_for_url,
page=page_number-1,
_external=True)
else:
previous_page_url=None
ifpaginated_objects.has_next:
next_page_url=url_for(
self.resource_for_url,
page=page_number+1,
_external=True)
else:
next_page_url=None
dumped_objects=self.schema.dump(objects,many=True).data
return({
self.key_name:dumped_objects,
'previous':previous_page_url,
'next':next_page_url,
'count':paginated_objects.total
})
ThePaginationHelperclassdeclaresaconstructor,thatis,the__init__methodthatreceivedmanyargumentsandusesthemtoinitializetheattributeswiththesamenames:
request:TheFlaskrequestobjectthatwillallowthepaginate_querymethodtoretrievethepagenumbervaluespecifiedwiththeHTTPrequest.query:TheSQLAlchemyquerythatthepaginate_querymethodhastopaginate.resource_for_url:Astringwiththeresourcenamethatthepaginate_querymethodwillusetogeneratethefullURLsforthepreviouspageandthenextpage.key_name:Astringwiththekeynamethatthepaginate_querymethodwillusetoreturntheserializedobjects.schema:TheFlask-MarshmallowSchemasubclassthatthepaginate_querymethodmustusetoserializetheobjects.
Inaddition,theconstructorreadsandsavesthevaluesfortheconfigurationvariablesweaddedtotheconfig.pyfileintheresults_per_pageandpage_argument_nameattributes.
Theclassdeclaresthepaginate_querymethod.First,thecoderetrievesthepagenumberspecifiedintherequestandsavesitinthepage_numbervariable.Incasenopagenumberisspecified,thecodeassumesthatrequestrequiresthefirstpage.Then,thecodecallstheself.query.paginatemethodtoretrievethepagenumberspecifiedbypage_numberofthepaginatedresultofobjectsfromthedatabase,withanumberofresultsperpageindicatedbythevalueoftheself.results_per_pageattribute.Thenextlinesavesthepaginateditemsfromthepaginated_object.itemsattributeintheobjectsvariable.
Ifthevalueforthepaginated_objects.has_prevattributeisTrue,itmeansthatthereisa
previouspageavailable.Inthiscase,thecodecallstheflask.url_forfunctiontogeneratethefullURLforthepreviouspagewiththevalueoftheself.resource_for_urlattribute.The_externalargumentissettoTruebecausewewanttoprovidethefullURL.
Ifthevalueforthepaginated_objects.has_nextattributeisTrue,itmeansthatthereisanextpageavailable.Inthiscase,thecodecallstheflask.url_forfunctiontogeneratethefullURLforthenextpagewiththevalueoftheself.resource_for_urlattribute.
Then,thecodecallstheself.schema.dumpmethodtoserializethepartialresultspreviouslysavedintheobjectvariable,withthemanyargumentsettoTrue.Thedumped_objectsvariablesavesthereferencetothedataattributeoftheresultsreturnedbythecalltothedumpmethod.
Finally,themethodreturnsadictionarywiththefollowingkey-valuepairs:
self.key_name:Theserializedpartialresultssavedinthedumped_objectsvariable.'previous':ThefullURLforthepreviouspagesavedintheprevious_page_urlvariable.'previous':ThefullURLforthenextpagesavedinthenext_page_urlvariable.'count':Thetotalnumberofobjectsavailableinthecompleteresultsetretrievedfromthepaginated_objects.totalattribute.
AddingpaginationfeaturesOpentheapi/views.pyfileandreplacethecodefortheMessageListResource.getmethodwiththehighlightedlinesinthenextlisting.Inaddition,makesurethatyouaddtheimportstatement.Thecodefileforthesampleisincludedintherestful_python_chapter_07_01folder:
fromhelpersimportPaginationHelper
classMessageListResource(Resource):
defget(self):
pagination_helper=PaginationHelper(
request,
query=Message.query,
resource_for_url='api.messagelistresource',
key_name='results',
schema=message_schema)
result=pagination_helper.paginate_query()
returnresult
ThenewcodeforthegetmethodcreatesaninstanceofthepreviouslyexplainedPaginationHelperclassnamedpagination_helperwiththerequestobjectasthefirstargument.Thenamedargumentsspecifythequery,resource_for_url,key_name,andschemathatthePaginationHelperinstancehastousetoprovideapaginatedqueryresult.
Thenextlinecallsthepagination_helper.paginate_querymethodthatwillreturntheresultsofthepaginatedquerywiththepagenumberspecifiedintherequest.Finally,themethodreturnstheresultsofthecalltothismethodthatincludethepreviouslyexplaineddictionary.Inthiscase,thepaginatedresultsetwiththemessageswillberenderedasavalueofthe'results'key,specifiedinthekey_nameargument.
Now,wewillcomposeandsendanHTTPrequesttoretrieveallthemessages,specificallyanHTTPGETmethodto/api/messages/.
http:5000/api/messages/
Thefollowingistheequivalentcurlcommand:
curl-iXGET:5000/api/messages/
ThenewcodefortheMessageListResource.getmethodwillworkwithpaginationandtheresultwillprovideusthefirst5messages(resultskey),thetotalnumberofmessagesforthequery(countkey)andalinktothenext(nextkey)andprevious(previouskey)pages.Inthiscase,theresultsetisthefirstpage,andtherefore,thelinktothepreviouspage(previouskey)isnull.Wewillreceivea200OKstatuscodeintheresponseheaderandthe5messagesintheresultsarray:
HTTP/1.0200OK
Content-Length:2521
Content-Type:application/json
Date:Wed,10Aug201618:26:44GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"count":12,
"results":[
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:27:30.124511+00:00",
"duration":8,
"id":2,
"message":"Checkinglightsensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/2"
},
{
"category":{
"id":3,
"name":"Error",
"url":"http://localhost:5000/api/categories/3"
},
"creation_date":"2016-08-08T14:20:22.103752+00:00",
"duration":10,
"id":3,
"message":"Temperaturesensorerror",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/3"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-08T12:18:43.260474+00:00",
"duration":5,
"id":1,
"message":"Checkingtemperaturesensor",
"printed_once":true,
"printed_times":1,
"url":"http://localhost:5000/api/messages/1"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:18:26.648071+00:00",
"duration":25,
"id":4,
"message":"Initializinglightcontroller",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/4"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:16.174807+00:00",
"duration":20,
"id":5,
"message":"Initializinglightsensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/5"
}
],
"next":"http://localhost:5000/api/messages/?page=2",
"previous":null
}
InthepreviousHTTPrequest,wedidn'tspecifyanyvalueforthepageparameter,andthereforethepaginate_querymethodinthePaginationHelperclassrequeststhefirstpagetothepaginatedquery.IfwecomposeandsendthefollowingHTTPrequesttoretrievethefirstpageofallthemessagesbyspecifying1forthepagevalue,theAPIwillprovidethesameresultsshownbefore:
http':5000/api/messages/?page=1'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':5000/api/messages/?page=1'
Tip
ThecodeinthePaginationHelperclassconsidersthatfirstpageispagenumber1.Thus,wedon'tworkwithzero-basednumberingforpages.
Now,wewillcomposeandsendanHTTPrequesttoretrievethenextpage,thatis,thesecondpageforthemessages,specificallyanHTTPGETmethodto/api/messages/withthepagevaluesetto2.RememberthatthevalueforthenextkeyreturnedintheJSONbodyofthepreviousresultprovidesuswiththefullURLtothenextpage:
http':5000/api/messages/?page=2'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':5000/api/messages/?page=2'
Theresultwillprovideusthesecondsetofthefivemessageresource(resultskey),thetotalnumberofmessagesforthequery(countkey),alinktothenext(nextkey),andprevious(previouskey)pages.Inthiscase,theresultsetisthesecondpage,andtherefore,thelinktothepreviouspage(previouskey)ishttp://localhost:5000/api/messages/?page=1.Wewillreceivea200OKstatuscodeintheresponseheaderandthe5messagesintheresultsarray.
HTTP/1.0200OK
Content-Length:2557
Content-Type:application/json
Date:Wed,10Aug201619:51:50GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"count":12,
"next":"http://localhost:5000/api/messages/?page=3",
"previous":"http://localhost:5000/api/messages/?page=1",
"results":[
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:22.335600+00:00",
"duration":18,
"id":6,
"message":"Checkingpressuresensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/6"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:26.189009+00:00",
"duration":14,
"id":7,
"message":"Checkinggassensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/7"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:29.854576+00:00",
"duration":22,
"id":8,
"message":"SettingADCresolution",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/8"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:33.838977+00:00",
"duration":15,
"id":9,
"message":"Settingsamplerate",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/9"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:37.830843+00:00",
"duration":18,
"id":10,
"message":"Initializingpressuresensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/10"
}
]
}
Finally,wewillcomposeandsendanHTTPrequesttoretrievethelastpage,thatis,thethirdpageforthemessages,specificallyanHTTPGETmethodto/api/messages/withthepagevaluesetto3.RememberthatthevalueforthenextkeyreturnedintheJSONbodyofthepreviousresultprovidesuswiththeURLtothenextpage:
http':5000/api/messages/?page=3'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':5000/api/messages/?page=3'
Theresultwillprovideusthelastsetwithtwomessageresources(resultskey),thetotalnumberofmessagesforthequery(countkey),alinktothenext(nextkey),andprevious(previouskey)pages.Inthiscase,theresultsetisthelastpage,andtherefore,thelinktothenextpage(nextkey)isnull.Wewillreceivea200OKstatuscodeintheresponseheaderandthe2messagesintheresultsarray:
HTTP/1.0200OK
Content-Length:1090
Content-Type:application/json
Date:Wed,10Aug201620:02:00GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"count":12,
"next":null,
"previous":"http://localhost:5000/api/messages/?page=2",
"results":[
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:41.645628+00:00",
"duration":16,
"id":11,
"message":"Initializinggassensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/11"
},
{
"category":{
"id":1,
"name":"Information",
"url":"http://localhost:5000/api/categories/1"
},
"creation_date":"2016-08-09T20:19:45.304391+00:00",
"duration":5,
"id":12,
"message":"Initializingproximitysensor",
"printed_once":false,
"printed_times":0,
"url":"http://localhost:5000/api/messages/12"
}
]
}
UnderstandingthestepstoaddauthenticationandpermissionsOurcurrentversionoftheAPIprocessesalltheincomingrequestswithoutrequiringanykindofauthentication.WewilluseaFlaskextensionandotherpackagestouseanHTTPauthenticationschemetoidentifytheuserthatoriginatedtherequestorthetokenthatsignedtherequest.Then,wewillusethesecredentialstoapplythepermissionsthatwilldeterminewhethertherequestmustbepermittedornot.Unluckily,neitherFlasknorFlask-RESTfulprovidesanauthenticationframeworkthatwecaneasilyplugandconfigure.Thus,wewillhavetowritecodetoperformmanytasksrelatedtoauthenticationandpermissions.
Wewanttobeabletocreateanewuserwithoutanyauthentication.However,alltheotherAPIcallsareonlygoingtobeavailableforauthenticatedusers.
First,wewillinstallaFlaskextensiontomakeiteasierforustoworkwithHTTPauthentication,Flask-HTTPAuth,andapackagetoallowustohashapasswordandcheckwhetheraprovidedpasswordisvalidornot,passlib.
WewillcreateanewUsermodelthatwillrepresentauser.Themodelwillprovidemethodstoallowustohashapasswordandverifywhetherapasswordprovidedforauserisvalidornot.WewillcreateaUserSchemaclasstospecifyhowwewanttoserializeanddeserializeauser.
Then,wewillconfiguretheFlaskextensiontoworkwithourUsermodeltoverifypasswordsandsettheauthenticateduserassociatedwitharequest.Wewillmakechangestotheexistingresourcestorequireauthenticationandwewillnewresourcestoallowustoretrieveexistingusersandcreateanewone.Finally,wewillconfiguretheroutesfortheresourcesrelatedtousers.
Oncewehavecompletedthepreviouslymentionedtasks,wewillrunmigrationstogeneratethenewtablethatpersiststheusersinthedatabase.Then,wewillcomposeandsendHTTPrequeststounderstandhowtheauthenticationandpermissionsworkwithournewversionoftheAPI.
MakesureyouquittheFlaskdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheterminaloraCommandPromptwindowinwhichitisrunning.ItistimetorunmanycommandsthatwillbethesameforeithermacOS,Linux,orWindows.Wecaninstallallthenecessarypackageswithpipwithasinglecommand.However,wewillruntwoindependentcommandstomakeiteasiertodetectanyproblemsincaseaspecificinstallationfails.
Now,wemustrunthefollowingcommandtoinstallFlask-HTTPAuthwithpip.ThispackagemakesiteasytoaddbasicHTTPauthenticationtoanyFlaskapplication:
pipinstallFlask-HTTPAuth
ThelastlinesfortheoutputwillindicatetheFlask-HTTPAuthpackagehasbeensuccessfullyinstalled:
Installingcollectedpackages:Flask-HTTPAuth
Runningsetup.pyinstallforFlask-HTTPAuth
SuccessfullyinstalledFlask-HTTPAuth-3.2.1
Runthefollowingcommandtoinstallpasslibwithpip.Thispackageisapopularonethatprovidesacomprehensivepasswordhashingframeworkthatsupportsmorethan30schemes.Wedefinitelydon'twanttowriteourownerror-proneandprobablyhighlyinsecurepasswordhashingcode,andtherefore,wewilltakeadvantageofalibrarythatprovidestheseservices:
pipinstallpasslib
Thelastlinesfortheoutputwillindicatethepasslibpackagehasbeensuccessfullyinstalled:
Installingcollectedpackages:passlib
Successfullyinstalledpasslib-1.6.5
AddingausermodelNow,wewillcreatethemodelthatwewillusetorepresentandpersisttheuser.Opentheapi/models.pyfileandaddthefollowinglinesafterthedeclarationoftheAddUpdateDeleteclass.Makesurethatyouaddtheimportstatements.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
frompasslib.appsimportcustom_app_contextaspassword_context
importre
classUser(db.Model,AddUpdateDelete):
id=db.Column(db.Integer,primary_key=True)
name=db.Column(db.String(50),unique=True,nullable=False)
#Isavethehashedpassword
hashed_password=db.Column(db.String(120),nullable=False)
creation_date=db.Column(db.TIMESTAMP,
server_default=db.func.current_timestamp(),nullable=False)
defverify_password(self,password):
returnpassword_context.verify(password,self.hashed_password)
defcheck_password_strength_and_hash_if_ok(self,password):
iflen(password)<8:
return'Thepasswordistooshort',False
iflen(password)>32:
return'Thepasswordistoolong',False
ifre.search(r'[A-Z]',password)isNone:
return'Thepasswordmustincludeatleastoneuppercaseletter',
False
ifre.search(r'[a-z]',password)isNone:
return'Thepasswordmustincludeatleastonelowercaseletter',
False
ifre.search(r'\d',password)isNone:
return'Thepasswordmustincludeatleastonenumber',False
ifre.search(r"[!#$%&'()*+,-./[\\\]^_`{|}~"+r'"]',password)isNone:
return'Thepasswordmustincludeatleastonesymbol',False
self.hashed_password=password_context.encrypt(password)
return'',True
def__init__(self,name):
self.name=name
ThecodedeclarestheUsermodel,specificallyasubclassesofboththedb.ModelandtheAddUpdateDeleteclasses.Wespecifiedthefieldtypes,maximumlengthsanddefaultsforthefollowingthreeattributes-id,name,hashed_passwordandcreation_date.Theseattributesrepresentfieldswithoutanyrelationship,andtherefore,theyareinstancesofthedb.Columnclass.ThemodeldeclaresanidattributeandspecifiestheTruevaluefortheprimary_keyargumenttoindicateitistheprimarykey.SQLAlchemywillusethedatatogeneratethenecessarytableinthePostgreSQLdatabase.
TheUserclassdeclaresthefollowingmethods:
check_password_strength_and_hash_if_ok:Thismethodusestheremodulethatprovidesregularexpressionmatchingoperationstocheckwhetherthepasswordreceivedasanargumentfulfilsmanyqualitativerequirements.Thecoderequiresthepasswordtobelongerthaneightcharacters,withamaximumof32characters.Thepasswordmustincludeatleastoneuppercaseletter,onelowercaseletter,onenumber,andonesymbol.Thecodecheckstheresultsofmanycallstothere.searchmethodtodeterminewhetherthereceivedpasswordfulfilseachrequirement.Incaseanyoftherequirementsisn'tfulfilled,thecodereturnsatuplewithanerrormessageandFalse.Otherwise,thecodecallstheencryptmethodforthepasslib.apps.custom_app_contextinstanceimportedaspassword_context,withthereceivedpasswordasanargument.Theencryptmethodchoosesareasonablystrongschemebasedontheplatform,withthedefaultsettingsforroundsselectionandthecodesavesthehashedpasswordinthehash_passwordattribute.Finally,thecodereturnsatuplewithanemptystringandTrue,indicatingthatthepasswordfulfilledthequalitativerequirementsanditwashashed.
Tip
Bydefault,thepassliblibrarywillusetheSHA-512schemefor64-bitplatformsandSHA-256for32-bitplatforms.Inaddition,theminimumnumberofroundswillbesetto535,000.Wewillusethedefaultconfigurationvaluesforthisexample.However,youmusttakeintoaccountthatthesevaluesmightrequiretoomuchprocessingtimeforeachrequestthathastovalidatethepassword.Youshoulddefinitelyselectthemostappropriatealgorithmandnumberofroundsbasedonyoursecurityrequirements.
verify_password:Thismethodcallstheverifymethodforthepasslib.apps.custom_app_contextinstanceimportedaspassword_context,withthereceivedpasswordandthestoredhashedpasswordfortheuser,self.hashed_password,asthearguments.TheverifymethodhashesthereceivedpasswordandreturnsTrueonlyifthehashedreceivedpasswordmatchesthestoredhashedpassword.Weneverrestorethesavedpasswordtoitsoriginalstate.Wejustcomparehashedvalues.
Themodeldeclaresaconstructor,thatis,the__init__method.Thisconstructorreceivestheusernameinthenameargumentandsavesitinanattributewiththesamename.
Creatingaschemastovalidate,serialize,anddeserializeusersNow,wewillcreatetheFlask-Marshmallowschemathatwewillusetovalidate,serializeanddeserializethepreviouslydeclaredUsermodel.Opentheapi/models.pyfileandaddthefollowingcodeaftertheexistinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
classUserSchema(ma.Schema):
id=fields.Integer(dump_only=True)
name=fields.String(required=True,validate=validate.Length(3))
url=ma.URLFor('api.userresource',id='<id>',_external=True)
ThecodedeclarestheUserSchemaschema,specificallyasubclassofthema.Schemaclass.Rememberthatthepreviouscodewewrotefortheapi/models.pyfilecreatedaflask_marshmallow.Mashmallowinstancenamedma.
Wedeclaretheattributesthatrepresentfieldsasinstancesoftheappropriateclassdeclaredinthemarshmallow.fieldsmodule.TheUserSchemaclassdeclaresthenameattributeasaninstanceoffields.String.TherequiredargumentissettoTruetospecifythatthefieldcannotbeanemptystring.Thevalidateargumentissettovalidate.Length(3)tospecifythatthefieldmusthaveaminimumlengthof3characters.
Thevalidationforthepasswordisn'tincludedintheschema.Wewillusethecheck_password_strength_and_hash_if_okmethoddefinedintheUserclasstovalidatethepassword.
AddingauthenticationtoresourcesWewillconfiguretheFlask-HTTPAuthextensiontoworkwithourUsermodeltoverifypasswordsandsettheauthenticateduserassociatedwitharequest.Wewilldeclareacustomfunctionthatthisextensionwilluseasacallbacktoverifyapassword.Wewillcreateanewbaseclassforourresourcesthatwillrequireauthentication.Opentheapi/views.pyfileandaddthefollowingcodeafterthelastlinethatusestheimportstatementandbeforethelinesthatdeclarestheBlueprintinstance.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
fromflask_httpauthimportHTTPBasicAuth
fromflaskimportg
frommodelsimportUser,UserSchema
auth=HTTPBasicAuth()
@auth.verify_password
defverify_user_password(name,password):
user=User.query.filter_by(name=name).first()
ifnotuserornotuser.verify_password(password):
returnFalse
g.user=user
returnTrue
classAuthRequiredResource(Resource):
method_decorators=[auth.login_required]
First,wecreateaninstanceoftheflask_httpauth.HTTPBasicAuthclassnamedauth.Then,wedeclaretheverify_user_passwordfunctionthatreceivesanameandapasswordasarguments.Thefunctionusesthe@auth.verify_passworddecoratortomakethisfunctionbecomethecallbackthatFlask-HTTPAuthwillusetoverifythepasswordforaspecificuser.Thefunctionretrievestheuserwhosenamematchesthenamespecifiedintheargumentandsavesitsreferenceintheuservariable.Ifauserisfound,thecodecheckstheresultsoftheuser.verify_passwordmethodwiththereceivedpasswordasanargument.
Ifeitherauserisn'tfoundorthecalltouser.verify_passwordreturnsFalse,thefunctionreturnsFalseandtheauthenticationwillfail.Ifthecalltouser.verify_passwordreturnsTrue,thefunctionstorestheauthenticatedUserinstanceintheuserattributefortheflask.gobject.
Tip
Theflask.gobjectisaproxythatallowsustostoreonthiswhateverwewanttoshareforonerequestonly.Theuserattributeweaddedtotheflask.gobjectwillbeonlyvalidfortheactiverequestanditwillreturndifferentvaluesforeachdifferentrequest.Thisway,itis
possibletouseflask.g.userinanotherfunctionormethodcalledduringarequesttoaccessdetailsabouttheauthenticateduserfortherequest.
Finally,wedeclaredtheAuthRequiredResourceclassasasubclassofflask_restful.Resource.Wejustspecifiedauth.login_requiredasoneofthemembersofthelistthatweassigntothemethod_decoratorspropertyinheritedfromthebaseclass.Thisway,allthemethodsdeclaredinaresourcethatusesthenewAuthRequiredResourceclassasitssuperclasswillhavetheauth.login_requireddecoratorappliedtothem,andtherefore,anymethodthatiscalledtotheresourcewillrequireauthentication.
Now,wewillreplacethebaseclassfortheexistingresourceclassestomaketheminheritfromAuthRequiredResourceinsteadofResource.Wewantanyoftherequeststhatretrieveormodifycategoriesandmessagestobeauthenticated.
Thefollowinglinesshowthedeclarationsforthefourresourceclasses:
classMessageResource(Resource):
classMessageListResource(Resource):
classCategoryResource(Resource):
classCategoryListResource(Resource):
Opentheapi/views.pyfileandreplaceResourcebyAuthRequiredResourceinthepreviouslyshownfourlinesthatdeclaretheresourceclasses.Thefollowinglinesshowthenewcodeforeachresourceclassdeclaration:
classMessageResource(AuthRequiredResource):
classMessageListResource(AuthRequiredResource):
classCategoryResource(AuthRequiredResource):
classCategoryListResource(AuthRequiredResource):
CreatingresourceclassestohandleusersWejustwanttobeabletocreateusersandusethemtoauthenticaterequests.Thus,wewilljustfocusoncreatingresourceclasseswithjustafewmethods.Wewon'tcreateacompleteusermanagementsystem.
Wewillcreatetheresourceclassesthatrepresenttheuserandthecollectionofusers.First,wewillcreateaUserResourceclassthatwewillusetorepresentauserresource.Opentheapi/views.pyfileandaddthefollowinglinesafterthelinethatcreatestheApiinstance.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
classUserResource(AuthRequiredResource):
defget(self,id):
user=User.query.get_or_404(id)
result=user_schema.dump(user).data
returnresult
TheUserResourceclassisasubclassofthepreviouslycodedAuthRequiredResourceanddeclaresagetmethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource.Themethodreceivestheidoftheuserthathastoberetrievedintheidargument.ThecodecallstheUser.query.get_or_404methodtoreturnanHTTP404NotFoundstatusincasethereisnouserwiththerequestedidintheunderlyingdatabase.Incasetheuserexists,thecodecallstheuser_schema.dumpmethodwiththeretrieveduserasanargumenttousetheUserSchemainstancetoserializetheUserinstancewhoseidmatchesthespecifiedid.ThedumpmethodtakestheUserinstanceandappliesthefieldfilteringandoutputformattingspecifiedintheUserSchemaclass.Thefieldfilteringspecifiesthatwedon'twantthehashedpasswordtobeserialized.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessageinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.
Now,wewillcreateaUserListResourceclassthatwewillusetorepresentthecollectionofusers.Opentheapi/views.pyfileandaddthefollowinglinesafterthecodethatcreatestheUserResourceclass.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
classUserListResource(Resource):
@auth.login_required
defget(self):
pagination_helper=PaginationHelper(
request,
query=User.query,
resource_for_url='api.userlistresource',
key_name='results',
schema=user_schema)
result=pagination_helper.paginate_query()
returnresult
defpost(self):
request_dict=request.get_json()
ifnotrequest_dict:
response={'user':'Noinputdataprovided'}
returnresponse,status.HTTP_400_BAD_REQUEST
errors=user_schema.validate(request_dict)
iferrors:
returnerrors,status.HTTP_400_BAD_REQUEST
name=request_dict['name']
existing_user=User.query.filter_by(name=name).first()
ifexisting_userisnotNone:
response={'user':'Anuserwiththesamenamealreadyexists'}
returnresponse,status.HTTP_400_BAD_REQUEST
try:
user=User(name=name)
error_message,password_ok=\
user.check_password_strength_and_hash_if_ok(request_dict['password'])
ifpassword_ok:
user.add(user)
query=User.query.get(user.id)
result=user_schema.dump(query).data
returnresult,status.HTTP_201_CREATED
else:
return{"error":error_message},status.HTTP_400_BAD_REQUEST
exceptSQLAlchemyErrorase:
db.session.rollback()
resp={"error":str(e)}
returnresp,status.HTTP_400_BAD_REQUEST
TheUserListResourceclassisasubclassofflask_restful.Resourcebecausewedon'twantallthemethodstorequireauthentication.Wewanttobeabletocreateanewuserwithoutbeingauthenticated,andtherefore,[email protected]_requireddecoratoronlyforthegetmethod.Thepostmethoddoesn'trequireauthentication.TheclassdeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestontherepresentedresource:
get:ThismethodreturnsalistwithalltheUserinstancessavedinthedatabase.First,thecodecallstheUser.query.allmethodtoretrievealltheUserinstancespersistedinthedatabase.Then,thecodecallstheuser_schema.dumpmethodwiththeretrievedmessagesandthemanyargumentsettoTruetoserializetheiterablecollectionofobjects.ThedumpmethodwilltakeeachUserinstanceretrievedfromthedatabaseandapplythefieldfilteringandoutputformattingspecifiedtheCategorySchemaclass.Thecodereturnsthedataattributeoftheresultreturnedbythedumpmethod,thatis,theserializedmessagesinJSONformatasthebody,withthedefaultHTTP200OKstatuscode.post:Thismethodretrievesthekey-valuepairsreceivedintheJSONbody,createsanewUserinstanceandpersistsitinthedatabase.First,thecodecallstherequest.get_jsonmethodtoretrievethekey-valuepairsreceivedasargumentswiththerequest.Then,thecodecallstheuser_schema.validatemethodtovalidatethenewuserbuiltwiththeretrievedkey-valuepairs.Inthiscase,thecalltothismethodwilljustvalidatethenamefieldfortheuser.Incasetherewerevalidationerrors,thecodereturnsthevalidationerrorsandanHTTP400BadRequeststatus.Ifthevalidationissuccessful,thecode
checkswhetheranuserwiththesamenamealreadyexistsinthedatabaseornottoreturnanappropriateerrorforthefieldthatmustbeunique.Iftheusernameisunique,thecodecreatesanewuserwiththespecifiednameandcallsitscheck_password_strength_and_hash_if_okmethod.Iftheprovidedpasswordfulfilsallthequalityrequirements,thecodepersiststheuserwithitshashedpasswordinthedatabase.Finally,thecodereturnstheserializedsaveduserinJSONformatasthebody,withtheHTTP201Createdstatuscode:
ThefollowingtableshowsthemethodofourpreviouslycreatedclassesrelatedtousresthatwewanttobeexecutedforeachcombinationofHTTPverbandscope.
HTTPverb Scope Classandmethod Requiresauthentication
GET Collectionofusers UserListResource.get Yes
GET User UserResource.get Yes
POST Collectionofusers UserListResource.post No
WemustmakethenecessaryresourceroutingconfigurationstocalltheappropriatemethodsandpassthemallthenecessaryargumentsbydefiningURLrules.Thefollowinglinesconfiguretheresourceroutingfortheuserrelatedresourcestotheapi.Opentheapi/views.pyfileandaddthefollowinglinesattheendofthecode.Thecodefileforthesampleisincludedintherestful_python_chapter_07_02folder:
api.add_resource(UserListResource,'/users/')
api.add_resource(UserResource,'/users/<int:id>')
Eachcalltotheapi.add_resourcemethodroutesaURLtooneofthepreviouslycodeduserrelatedresources.WhenthereisarequesttotheAPIandtheURLmatchesoneoftheURLsspecifiedintheapi.add_resourcemethod,FlaskwillcallthemethodthatmatchestheHTTPverbintherequestforthespecifiedclass.
RunningmigrationstogeneratetheusertableNow,wewillrunmanyscriptstorunmigrationsandgeneratethenecessarytableinthePostgreSQLdatabase.MakesureyourunthescriptsintheterminalortheCommandPromptwindowinwhichyouhaveactivatedthevirtualenvironmentandthatyouarelocatedintheapifolder.
Runthefirstscriptthatpopulatesthemigrationscriptwiththedetectedchangesinthemodels.Inthiscase,itisthesecondtimewepopulatethemigrationscript,andtherefore,themigrationscriptwillgeneratethenewtablethatwillpersistournewUsermodel:model:
pythonmigrate.pydbmigrate
Thefollowinglinesshowthesampleoutputgeneratedafterrunningthepreviousscript.Youroutputwillbedifferentaccordingtothebasefolderinwhichyouhavecreatedthevirtualenvironment.
INFO[alembic.runtime.migration]ContextimplPostgresqlImpl.
INFO[alembic.runtime.migration]WillassumetransactionalDDL.
INFO[alembic.autogenerate.compare]Detectedaddedtable'user'
INFO[alembic.ddl.postgresql]Detectedsequencenamed'message_id_seq'as
ownedbyintegercolumn'message(id)',assumingSERIALandomitting
Generating
/Users/gaston/PythonREST/Flask02/api/migrations/versions/c8c45e615f6d_.py
...done
Theoutputindicatesthattheapi/migrations/versions/c8c45e615f6d_.pyfileincludesthecodetocreatetheusertables.Thefollowinglinesshowthecodeforthisfilethatwasautomaticallygeneratedbasedonthemodels.Noticethatthefilenamewillbedifferentinyourconfiguration.Thecodefileforthesampleisincludedintherestful_python_chapter_06_01folder:
"""emptymessage
RevisionID:c8c45e615f6d
Revises:417543056ac3
CreateDate:2016-08-1117:31:44.989313
"""
#revisionidentifiers,usedbyAlembic.
revision='c8c45e615f6d'
down_revision='417543056ac3'
fromalembicimportop
importsqlalchemyassa
defupgrade():
###commandsautogeneratedbyAlembic-pleaseadjust!###
op.create_table('user',
sa.Column('id',sa.Integer(),nullable=False),
sa.Column('name',sa.String(length=50),nullable=False),
sa.Column('hashed_password',sa.String(length=120),nullable=False),
sa.Column('creation_date',sa.TIMESTAMP(),
server_default=sa.text('CURRENT_TIMESTAMP'),nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
###endAlembiccommands###
defdowngrade():
###commandsautogeneratedbyAlembic-pleaseadjust!###
op.drop_table('user')
###endAlembiccommands###
Thecodedefinestwofunctions:upgradeanddowngrade.Theupgradefunctionrunsthenecessarycodetocreatetheusertablebymakingcallstoalembic.op.create_table.Thedowngradefunctionrunsthenecessarycodetogobacktothepreviousversion.
Runthesecondscripttoupgradethedatabase:
pythonmigrate.pydbupgrade
Thefollowinglinesshowthesampleoutputgeneratedafterrunningthepreviousscript:
INFO[alembic.runtime.migration]ContextimplPostgresqlImpl.
INFO[alembic.runtime.migration]WillassumetransactionalDDL.
INFO[alembic.runtime.migration]Runningupgrade417543056ac3->
c8c45e615f6d,emptymessage
Thepreviousscriptcalledtheupgradefunctiondefinedintheautomaticallygeneratedapi/migrations/versions/c8c45e615f6d_.pyscript.Don'tforgetthatthefilenamewillbedifferentinyourconfiguration.
Afterwerunthepreviousscripts,wecanusethePostgreSQLcommandlineoranyotherapplicationthatallowsustoeasilyverifythecontentsofthePostreSQLdatabasetocheckthenewtablethatthemigrationgenerated.Runthefollowingcommandtolistthegeneratedtables.Incasethedatabasenameyouareusingisnotnamedmessages,makesureyouusetheappropriatedatabasename:
psql--username=user_name--dbname=messages--command="\dt"
Thefollowinglinesshowtheoutputwithallthegeneratedtablenames.Themigrationsupgradegenerateanewtablenameduser.
Listofrelations
Schema|Name|Type|Owner
--------+-----------------+-------+-----------
public|alembic_version|table|user_name
public|category|table|user_name
public|message|table|user_name
public|user|table|user_name
(4rows)
SQLAlchemygeneratedtheusertablewithitsprimarykey,itsuniqueconstraintonthenamefieldandthepasswordfieldbasedontheinformationincludedinourUsermodel.
ThefollowingcommandwillallowyoutocheckthecontentsoftheusertableafterwecomposeandsendHTTPrequeststotheRESTfulAPIandcreatenewusers.ThecommandsassumethatyouarerunningPostgreSQLonthesamecomputerinwhichyouarerunningthecommand:
psql--username=user_name--dbname=messages--command="SELECT*FROM
public.user;"
Now,wecanruntheapi/run.pyscriptthatlaunchesFlask'sdevelopment.Executethefollowingcommandintheapifolder:
pythonrun.py
Afterweexecutethepreviouscommand,thedevelopmentserverwillstartlisteningatport5000.
ComposingrequestswiththenecessaryauthenticationNow,wewillcomposeandsendanHTTPrequesttoretrievethefirstpageofthemessageswithoutauthenticationcredentials:
httpPOST':5000/api/messages/?page=1'
Thefollowingistheequivalentcurlcommand:
curl-iXGET':5000/api/messages/?page=1'
Wewillreceivea401Unauthorizedstatuscodeintheresponseheader.Thefollowinglinesshowasampleresponse:
HTTP/1.0401UNAUTHORIZED
Content-Length:19
Content-Type:text/html;charset=utf-8
Date:Mon,15Aug201601:16:36GMT
Server:Werkzeug/0.11.10Python/3.5.1
WWW-Authenticate:Basicrealm="AuthenticationRequired"
Ifwewanttoretrievemessages,thatis,tomakeaGETrequestto/api/messages/,weneedtoprovideauthenticationcredentialsusingHTTPauthentication.However,beforewecandothis,itisnecessarytocreateanewuser.Wewillusethenewusertotestournewresourceclassesrelatedtousersandourchangesinthepermissionspolicies.
httpPOST:5000/api/users/name='brandon'password='brandonpassword'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"brandon",
"password":"brandonpassword"}':5000/api/users/
Tip
Ofcourse,thecreationofauserandtheexecutionofthemethodsthatrequireauthenticationshouldonlybepossibleunderHTTPS.Thisway,theusernameandthepasswordwouldbeencrypted.
ThepreviouscommandwillcomposeandsendaPOSTHTTPrequestwiththespecifiedJSONkey-valuepairs.Therequestsspecify/api/user/,andtherefore,itwillmatchthe'/users/'URLroutefortheUserListresourceandruntheUserList.postmethodthatdoesn'trequireauthentication.Themethoddoesn'treceiveargumentsbecausetheURLroutedoesn'tincludeanyparameters.AstheHTTPverbfortherequestisPOST,Flaskcallsthepostmethod.
Thepreviouslyspecifiedpasswordonlyincludeslowercaseletters,andtherefore,itdoesn'tfulfilallthequalitativerequirementswehavespecifiedforthepasswordsinthe
User.check_password_strength_and_hash_if_okmethod.Thus,Wewillreceivea400BadRequeststatuscodeintheresponseheaderandtheerrormessageindicatingtherequirementthatthepassworddidn'tfulfilintheJSONbody.Thefollowinglinesshowasampleresponse:
HTTP/1.0400BADREQUEST
Content-Length:75
Content-Type:application/json
Date:Mon,15Aug201601:29:55GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"error":"Thepasswordmustincludeatleastoneuppercaseletter"
}
Thefollowingcommandwillcreateauserwithavalidpassword:
httpPOST:5000/api/users/name='brandon'password='iA4!V3riS#c^R9'
Thefollowingistheequivalentcurlcommand:
curl-iXPOST-H"Content-Type:application/json"-d'{"name":"brandon",
"password":"iA4!V3riS#c^R9"}':5000/api/users/
IfthenewUserinstanceissuccessfullypersistedinthedatabase,thecallwillreturnanHTTP201CreatedstatuscodeandtherecentlypersistedUserserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequests,withthenewUserobjectintheJSONresponses.NotethattheresponseincludestheURL,url,forthecreateduseranddoesn'tincludeanyinformationrelatedtothepassword.
HTTP/1.0201CREATED
Content-Length:87
Content-Type:application/json
Date:Mon,15Aug201601:33:23GMT
Server:Werkzeug/0.11.10Python/3.5.1
{
"id":1,
"name":"brandon",
"url":"http://localhost:5000/api/users/1"
}
WecanrunthepreviouslyexplainedcommandtocheckthecontentsoftheusertablethatthemigrationscreatedinthePostgreSQLdatabase.Wewillnoticethatthehashed_passwordfieldcontentsarehashedforthenewrowintheusertable.ThefollowingscreenshotshowsthecontentsforthenewrowoftheusertableinaPostgreSQLdatabaseafterrunningtheHTTPrequest:
Ifwewanttoretrievethefirstpageofmessages,thatis,tomakeaGETrequestto/api/messages/,weneedtoprovideauthenticationcredentialsusingHTTPauthentication.Now,wewillcomposeandsendanHTTPrequesttoretrievethefirstpageofmessageswithauthenticationcredentials,thatis,withtheusernameandthepasswordwehaverecentlycreated:
http-a'brandon':'iA4!V3riS#c^R9'':5000/api/messages/?page=1'
Thefollowingistheequivalentcurlcommand:
curl--user'brandon':'iA4!V3riS#c^R9'-iXGET':5000/api/messages/?page=1'
Theuserwillbesuccessfullyauthenticatedandwewillbeabletoprocesstherequesttoretrievethefirstpageofmessages.WithallthechangeswehavemadetoourAPI,unauthenticatedrequestscanonlycreateanewuser.
Testyourknowledge1. Theflask.gobjectis:
1. Aproxythatprovidesaccesstothecurrentrequest.2. Aninstanceoftheflask_httpauth.HTTPBasicAuthclass.3. Aproxythatallowsustostoreonthiswhateverwewanttoshareforonerequest
only.
2. Thepasslibpackageprovides:1. Apasswordhashingframeworkthatsupportsmorethan30schemes.2. Anauthenticationframeworkthatautomaticallyaddsmodelsforusersand
permissiostoaFlaskapplication.3. AlightweightwebframeworkthatreplacesFlask.
3. Theauth.verify_passworddecoratorappliedtoafunction:1. MakesthisfunctionbecomethecallbackthatFlask-HTTPAuthwillusetohashthe
passwordforaspecificuser.2. MakesthisfunctionbecomethecallbackthatSQLAlchmeywillusetoverifythe
passwordforaspecificuser.3. MakesthisfunctionbecomethecallbackthatFlask-HTTPAuthwillusetoverifythe
passwordforaspecificuser.
4. Whenyouassignalistthatincludesauth.login_requiredtothemethod_decoratorspropertyofanysubclassofflask_restful.Resource,consideringthatauthisaninstanceoftheflask_httpauth.HTTPBasicAuth():1. Allthemethodsdeclaredintheresourcewillhavetheauth.login_required
decoratorappliedtothem.2. Thepostmethoddeclaredintheresourcewillhaveauth.login_required
decoratorappliedtoit.3. Anyofthefollowingmethodsdeclaredintheresourcewillhave
auth.login_requireddecoratorappliedtothem:delete,patch,postandput.
5. Whichofthefollowinglinesretrievestheintegervalueforthe'page'argumentfromtherequestobject,consideringthatthecodewouldberunningwithinamethoddefinedinasubclassofflask_restful.Resourceclass?1. page_number=request.get_argument('page',1,type=int)2. page_number=request.args.get('page',1,type=int)3. page_number=request.arguments.get('page',1,type=int)
SummaryInthischapter,weimprovedtheRESTfulAPIinmanyways.Weaddeduserfriendlyerrormessageswhenresourcesaren'tunique.WetestedhowtoupdatesingleormultiplefieldswiththePATCHmethodandwecreatedourowngenericpaginationclass.
Then,westartedworkingwithauthenticationandpermissions.Weaddedausermodelandweupdatedthedatabase.WemademanychangesinthedifferentpiecesofcodetoachieveaspecificsecuritygoalandwetookadvantageofFlask-HTTPAuthandpasslibtouseHTTPauthenticationinourAPI.
NowthatwehavebuiltanimprovedacomplexAPIthatusespaginationandauthentication,wewilluseadditionalabstractionsincludedintheframeworkandwewillcode,execute,andimproveunittest,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter8.TestingandDeployinganAPIwithFlaskInthischapter,wewillconfigure,write,andexecuteunittestsandlearnafewthingsrelatedtodeployment.Wewill:
SetupunittestsCreateadatabasefortestingWriteafirstroundofunittestsRununittestsandchecktestingcoverageImprovetestingcoverageUnderstandstrategiesfordeploymentsandscalability
SettingupunittestsWewillusenose2tomakeiteasiertodiscoverandrununittests.Wewillmeasuretestcoverage,andtherefore,wewillinstallthenecessarypackagetoallowustoruncoveragewithnose2.First,wewillinstallthenose2andcov-corepackagesinourvirtualenvironment.Thecov-corepackagewillallowustomeasuretestcoveragewithnose2.Then,wewillcreateanewPostgreSQLdatabasethatwewillusefortesting.Finally,wewillcreatetheconfigurationfileforthetestingenvironment.
MakesureyouquittheFlask'sdevelopmentserver.RememberthatyoujustneedtopressCtrl+CintheterminalortheCommandPromptwindowinwhichitisrunning.Wejustneedtorunthefollowingcommandtoinstallthenose2package:
pipinstallnose2
Thelastlinesoftheoutputwillindicatethatthedjango-nosepackagehasbeensuccessfullyinstalled.
Collectingnose2
Collectingsix>=1.1(fromnose2)
Downloadingsix-1.10.0-py2.py3-none-any.whl
Installingcollectedpackages:six,nose2
Successfullyinstallednose2-0.6.5six-1.10.0
Wejustneedtorunthefollowingcommandtoinstallthecov-corepackagethatwillalsoinstallthecoveragedependency:
pipinstallcov-core
Thelastlinesfortheoutputwillindicatethatthedjango-nosepackagehasbeensuccessfullyinstalled:
Collectingcov-core
Collectingcoverage>=3.6(fromcov-core)
Installingcollectedpackages:coverage,cov-core
Successfullyinstalledcov-core-1.15.0coverage-4.2
Now,wewillcreatethePostgreSQLdatabasethatwewilluseasarepositoryforourtestingenvironment.YouwillhavetodownloadandinstallaPostgreSQLdatabase,incaseyouaren'talreadyrunningitonthetestingenvironmentonyourcomputerorinatestingserver.
Tip
RemembertomakesurethatthePostgreSQLbinfolderisincludedinthePATHenvironmentalvariable.Youshouldbeabletoexecutethepsqlcommand-lineutilityfromyourcurrentTerminalorCommandPrompt.
WewillusethePostgreSQLcommand-linetoolstocreateanewdatabasenamed
test_messages.IncaseyoualreadyhaveaPostgreSQLdatabasewiththisname,makesurethatyouuseanothernameinallthecommandsandconfigurations.YoucanperformthesametaskwithanyPostgreSQLGUItool.IncaseyouaredevelopingonLinux,itisnecessarytorunthecommandsasthepostgresuser.RunthefollowingcommandinmacOSorWindowstocreateanewdatabasenamedtest_messages.Notethatthecommandwon'tgenerateanyoutput:
createdbtest_messages
InLinux,runthefollowingcommandtousethepostgresuser:
sudo-upostgrescreatedbtest_messages
Now,wewillusethepsqlcommand-linetooltorunsomeSQLstatementstograntprivilegesonthedatabasetoauser.Incaseyouareusingadifferentserverthanthedevelopmentserver,youwillhavetocreatetheuserbeforegrantingprivileges.InmacOSorWindows,runthefollowingcommandtolaunchpsql:
psql
InLinux,runthefollowingcommandtousethepostgresuser
sudo-upsql
Then,runthefollowingSQLstatementsandfinallyenter\qtoexitthepsqlcommand-linetool.Replaceuser_namewithyourdesiredusernametouseinthenewdatabaseandpasswordwithyourchosenpassword.WewillusetheusernameandpasswordintheFlasktestingconfiguration.Youdon'tneedtorunthestepsincaseyouarealreadyworkingwithaspecificuserinPostgreSQLandyouhavealreadygrantedprivilegestothedatabasefortheuser:
GRANTALLPRIVILEGESONDATABASEtest_messagesTOuser_name;
\q
Createanewtest_config.pyfilewithintheapifolder.ThefollowinglinesshowthecodethatdeclaresvariablesthatdeterminetheconfigurationforFlaskandSQLAlchemyforourtestingenvironment.TheSQL_ALCHEMY_DATABASE_URIvariablegeneratesaSQLAlchemyURIforthePostgreSQLdatabasethatwewillusetorunallthemigrationsbeforestartingtestsandwewilldropalltheelementsafterexecutingallthetests.MakesureyouspecifythedesiredtestdatabasenameinthevalueforDB_NAMEandthatyouconfiguretheuser,password,host,andportbasedonyourPostgreSQLconfigurationforthetestingenvironment.Incaseyoufollowedtheprevioussteps,usethesettingsspecifiedinthesesteps.Thecodefileforthesampleisincludedintherestful_python_chapter_08_01folder.
importos
basedir=os.path.abspath(os.path.dirname(__file__))
DEBUG=True
PORT=5000
HOST="127.0.0.1"
SQLALCHEMY_ECHO=False
SQLALCHEMY_TRACK_MODIFICATIONS=True
SQLALCHEMY_DATABASE_URI="postgresql://{DB_USER}:
{DB_PASS}@{DB_ADDR}/{DB_NAME}".format(DB_USER="user_name",DB_PASS="password",
DB_ADDR="127.0.0.1",DB_NAME="test_messages")
SQLALCHEMY_MIGRATE_REPO=os.path.join(basedir,'db_repository')
TESTING=True
SERVER_NAME='127.0.0.1:5000'
PAGINATION_PAGE_SIZE=5
PAGINATION_PAGE_ARGUMENT_NAME='page'
#DisableCSRFprotectioninthetestingconfiguration
WTF_CSRF_ENABLED=False
Aswedidwiththesimilartestfilewecreatedforourdevelopmentenvironment,wewillspecifythepreviouslycreatedmoduleasanargumenttoafunctionthatwillcreateaFlaskappthatwewillusefortesting.Thisway,wehaveonemodulethatspecifiesallthevaluesforthedifferentconfigurationvariablesforourtestingenvironmentandanothermodulethatcreatesaFlaskappforourtestingenvironment.Itisalsopossibletocreateaclasshierarchywithoneclassforeachenvironmentwewanttouse.However,inoursamplecase,itiseasiertocreateanewconfigurationfileforourtestingenvironment.
WritingafirstroundofunittestsNow,wewillwriteafirstroundofunittests.Specifically,wewillwriteunittestsrelatedtotheuserandmessagecategoryresources:UserResource,UserListResource,CategoryResource,andCategoryListResource.Createanewtestssub-folderwithintheapifolder.Then,createanewtest_views.pyfilewithinthenewapi/testssub-folder.Addthefollowinglines,thatdeclaremanyimportstatementsandthefirstmethodsfortheInitialTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_01folder:
fromappimportcreate_app
frombase64importb64encode
fromflaskimportcurrent_app,json,url_for
frommodelsimportdb,Category,Message,User
importstatus
fromunittestimportTestCase
classInitialTests(TestCase):
defsetUp(self):
self.app=create_app('test_config')
self.test_client=self.app.test_client()
self.app_context=self.app.app_context()
self.app_context.push()
self.test_user_name='testuser'
self.test_user_password='T3s!p4s5w0RDd12#'
db.create_all()
deftearDown(self):
db.session.remove()
db.drop_all()
self.app_context.pop()
defget_accept_content_type_headers(self):
return{
'Accept':'application/json',
'Content-Type':'application/json'
}
defget_authentication_headers(self,username,password):
authentication_headers=self.get_accept_content_type_headers()
authentication_headers['Authorization']=\
'Basic'+b64encode((username+':'+password).encode('utf-
8')).decode('utf-8')
returnauthentication_headers
TheInitialTestsclassisasubclassofunittest.TestCase.TheclassoverridesthesetUpmethodthatwillbeexecutedbeforeeachtestmethodruns.Themethodcallsthecreate_appfunction,declaredintheappmodule,with'test_config'asanargument.ThefunctionwillsetupaFlaskappwiththismoduleastheconfigurationfile,andtherefore,theappwillusethepreviouslycreatedconfigurationfilethatspecifiesthedesiredvaluesforourtestingdatabaseandenvironment.Then,thecodesetsthetestingattributefortherecentlycreatedapp
toTrueinorderfortheexceptiontopropagatetothetestclient.
Thenextlinecallstheself.app.test_clientmethodtocreateatestclientforthepreviouslycreatedFlaskapplicationandsavesthetestclientinthetest_clientattribute.WewillusethetestclientinourtestmethodstoeasilycomposeandsendrequeststoourAPI.Then,thecodesavesandpushestheapplicationcontextandcreatestwoattributeswiththeusernameandpasswordwewilluseforourtests.Finally,themethodcallsthedb.create_allmethodtocreateallthenecessarytablesinourtestdatabaseconfiguredinthetest_config.pyfile.
TheInitialTestsclassoverridesthetearDownmethodthatwillbeexecutedaftereachtestmethodruns.ThecoderemovestheSQLAlchemysession,dropsallthetablesthatwecreatedinthetestdatabasebeforestartingtheexecutionofthetests,andpopstheapplicationcontext.Thisway,aftereachtestfinishesitsexecution,thetestdatabasewillbeemptyagain.
Theget_accept_content_type_headersmethodbuildsandreturnsadictionary(dict)withthevaluesoftheAcceptandContent-Typeheaderkeyssetto'application/json'.Wewillcallthismethodinourtestswheneverwehavetobuildaheadertocomposeourrequestswithoutauthentication.
Theget_authentication_headersmethodcallsthepreviouslyexplainedget_accept_content_type_headersmethodtogeneratetheheaderkey-valuepairswithoutauthentication.Then,thecodeaddsthenecessaryvaluetotheAuthorizationkeywiththeappropriateencodingtoprovidetheusernameandpasswordreceivedintheusernameandpasswordarguments.Thelastlinereturnsthegenerateddictionarythatincludesauthenticationinformation.Wewillcallthismethodinourtestswheneverwehavetobuildaheadertocomposeourrequestswithauthentication.WewillusetheusernameandpasswordwestoredinattributesthesetUpmethod.
Openthepreviouslycreatedtest_views.pyfilewithinthenewapi/testssub-folder.AddthefollowinglinesthatdeclaremanymethodsfortheInitialTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_01folder.
deftest_request_without_authentication(self):
"""
Ensurewecannotaccessaresourcethatrequirestauthenticationwithout
anappropriateauthenticationheader
"""
response=self.test_client.get(
url_for('api.messagelistresource',_external=True),
headers=self.get_accept_content_type_headers())
self.assertTrue(response.status_code==status.HTTP_401_UNAUTHORIZED)
defcreate_user(self,name,password):
url=url_for('api.userlistresource',_external=True)
data={'name':name,'password':password}
response=self.test_client.post(
url,
headers=self.get_accept_content_type_headers(),
data=json.dumps(data))
returnresponse
defcreate_category(self,name):
url=url_for('api.categorylistresource',_external=True)
data={'name':name}
response=self.test_client.post(
url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password),
data=json.dumps(data))
returnresponse
deftest_create_and_retrieve_category(self):
"""
EnsurewecancreateanewCategoryandthenretrieveit
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_category_name='NewInformation'
post_response=self.create_category(new_category_name)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Category.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
self.assertEqual(post_response_data['name'],new_category_name)
new_category_url=post_response_data['url']
get_response=self.test_client.get(
new_category_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['name'],new_category_name)
Thetest_request_without_authenticationmethodtestswhetherwehavebeenrejectedaccesstoaresourcethatrequiresauthenticationwhenwedon'tprovideanappropriateauthenticationheaderwiththerequest.ThemethodusesthetestclienttocomposeandsendanHTTPGETrequesttotheURLgeneratedforthe'api.messagelistresource'resourcetoretrievethelistofmessages.Weneedanauthenticatedrequesttoretrievethelistofmessages.However,thecodecallstheget_authentication_headersmethodtosetthevaluefortheheadersargumentinthecalltoself.test_client.get,andtherefore,thecodegeneratesarequestwithoutauthentication.Finally,themethodusesassertTruetocheckthatthestatus_codefortheresponseisHTTP401Unauthorized(status.HTTP_401_UNAUTHORIZED).
Thecreate_usermethodusesthetestclienttocomposeandsendanHTTPPOSTrequesttotheURLgeneratedforthe'api.userlistresource'resourcetocreateanewuserwiththenameandpasswordreceivedasarguments.Wedon'tneedanauthenticatedrequesttocreateanewuser,andtherefore,thecodecallsthepreviouslyexplainedget_accept_content_type_headersmethodtosetthevaluefortheheadersargumentinthecalltoself.test_client.post.Finally,thecodereturnstheresponsefromthePOSTrequest.Wheneverwehavetocreateanauthenticatedrequest,wewillcallthecreate_usermethodto
createanewuser.
Thecreate_categorymethodusesthetestclienttocomposeandsendanHTTPPOSTrequesttotheURLgeneratedforthe'api.categorylistresource'resourcetocreateanewCategorywiththenamereceivedasanargument.WeneedanauthenticatedrequesttocreateanewCategory,andtherefore,thecodecallsthepreviouslyexplainedget_authentication_headersmethodtosetthevaluefortheheadersargumentinthecalltoself.test_client.post.Theusernameandpasswordaresettoself.test_user_nameandself.test_user_password.Finally,thecodereturnstheresponsefromthePOSTrequest.Wheneverwehavetocreateacategory,wewillcallthecreate_categorymethodaftertheappropriateuserthatauthenticatestherequesthasbeencreated.
Thetest_create_and_retrieve_categorymethodtestswhetherwecancreateanewCategoryandthenretrieveit.Themethodcallsthepreviouslyexplainedcreate_usermethodtocreateanewuserandthenuseittoauthenticatetheHTTPPOSTrequestgeneratedinthecreate_game_categorymethod.Then,thecodecomposesandsendsanHTTPGETmethodtoretrievetherecentlycreatedCategorywiththeURLreceivedintheresponseofthepreviousHTTPPOSTrequest.ThemethodusesassertEqualtocheckforthefollowingexpectedresults:
Thestatus_codefortheHTTPPOSTresponseisHTTP201Created(status.HTTP_201_CREATED)ThetotalnumberofCategoryobjectsretrievedfromthedatabaseis1Thestatus_codefortheHTTPGETresponseisHTTP200OK(status.HTTP_200_OK)ThevalueforthenamekeyintheHTTPGETresponseisequaltothenamespecifiedforthenewcategory
Openthepreviouslycreatedtest_views.pyfilewithinthenewapi/testssub-folder.AddthefollowinglinesthatdeclaremanymethodsfortheInitialTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_01folder.
deftest_create_duplicated_category(self):
"""
EnsurewecannotcreateaduplicatedCategory
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_category_name='NewInformation'
post_response=self.create_category(new_category_name)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Category.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
self.assertEqual(post_response_data['name'],new_category_name)
second_post_response=self.create_category(new_category_name)
self.assertEqual(second_post_response.status_code,
status.HTTP_400_BAD_REQUEST)
self.assertEqual(Category.query.count(),1)
deftest_retrieve_categories_list(self):
"""
Ensurewecanretrievethecategorieslist
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_category_name_1='Error'
post_response_1=self.create_category(new_category_name_1)
self.assertEqual(post_response_1.status_code,status.HTTP_201_CREATED)
new_category_name_2='Warning'
post_response_2=self.create_category(new_category_name_2)
self.assertEqual(post_response_2.status_code,status.HTTP_201_CREATED)
url=url_for('api.categorylistresource',_external=True)
get_response=self.test_client.get(
url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(len(get_response_data),2)
self.assertEqual(get_response_data[0]['name'],new_category_name_1)
self.assertEqual(get_response_data[1]['name'],new_category_name_2)
"""
Ensurewecanupdatethenameforanexistingcategory
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_category_name_1='Error1'
post_response_1=self.create_category(new_category_name_1)
self.assertEqual(post_response_1.status_code,status.HTTP_201_CREATED)
post_response_data_1=json.loads(post_response_1.get_data(as_text=True))
new_category_url=post_response_data_1['url']
new_category_name_2='Error2'
data={'name':new_category_name_2}
patch_response=self.test_client.patch(
new_category_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password),
data=json.dumps(data))
self.assertEqual(patch_response.status_code,status.HTTP_200_OK)
get_response=self.test_client.get(
new_category_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['name'],new_category_name_2)
Theclassdeclaresthefollowingmethodswhosenamestartwiththetest_prefix:
test_create_duplicated_category:Testswhethertheuniqueconstraintsdon'tmakeitpossibleforustocreatetwocategorieswiththesamenameornot.ThesecondtimewecomposeandsendanHTTPPOSTrequestwithaduplicatecategoryname,wemustreceiveanHTTP400BadRequeststatuscode(status.HTTP_400_BAD_REQUEST)andthetotalnumberofCategoryobjectsretrievedfromthedatabasemustbe1.test_retrieve_categories_list:Testswhetherwecanretrievethecategorieslistornot.First,themethodcreatestwocategoriesandthenitmakessurethattheretrievedlistincludesthetwocreatedcategories.test_update_game_category:Testswhetherwecanupdateasinglefieldforacategory,specifically,itsnamefield.Thecodemakessurethatthenamehasbeenupdated.
Tip
Notethateachtestthatrequiresaspecificconditioninthedatabasemustexecuteallthenecessarycodeforthedatabasetobeinthisspecificcondition.Forexample,inordertoupdateanexistingcategory,firstwemustcreateanewcategoryandthenwecanupdateit.Eachtestmethodwillbeexecutedwithoutdatafromthepreviouslyexecutedtestmethodsinthedatabase,thatis,eachtestwillrunwithadatabasecleanedofdatafromprevioustests.
Runningunittestswithnose2andcheckingtestingcoverageNow,runthefollowingcommandtocreateallthenecessarytablesinourtestdatabaseandusethenose2testrunningtoexecuteallthetestswecreated.ThetestrunnerwillexecuteallthemethodsforourInitialTestsclassthatstartwiththetest_prefixandwilldisplaytheresults.
Tip
Thetestswon'tmakechangestothedatabasewehavebeenusingwhenworkingontheAPI.Rememberthatweconfiguredthetest_messagesdatabaseasourtestdatabase.
Removetheapi.pyfilewecreatedinthepreviouschapterfromtheapifolderbecausewedon'twantthetestscoveragetotakeintoaccountthisfile.Gototheapifolderandrunthefollowingcommandwithinthesamevirtualenvironmentthatwehavebeenusing.Wewillusethe-voptiontoinstructnose2toprinttestcasenamesandstatuses.The--with-coverageoptionturnsontestcoveragereportinggeneration:
nose2-v--with-coverage
Thefollowinglinesshowthesampleoutput.
test_create_and_retrieve_category(test_views.InitialTests)...ok
test_create_duplicated_category(test_views.InitialTests)...ok
test_request_without_authentication(test_views.InitialTests)...ok
test_retrieve_categories_list(test_views.InitialTests)...ok
test_update_category(test_views.InitialTests)...ok
--------------------------------------------------------
Ran5testsin3.973s
OK
-----------coverage:platformwin32,python3.5.2-final-0--
NameStmtsMissCover
-----------------------------------------
app.py90100%
config.py11110%
helpers.py231822%
migrate.py990%
models.py1012773%
run.py440%
status.py56591%
test_config.py120100%
tests\test_views.py960100%
views.py20410947%
-----------------------------------------
TOTAL52518365%
Bydefault,nose2looksformoduleswhosenamesstartwiththetestprefix.Inthiscase,theonlymodulethatmatchesthecriteriaisthetest_viewsmodule.Inthemodulesthatmatchthe
criteria,nose2loadstestsfromallthesubclassesofunittest.TestCaseandthefunctionswhosenamesstartwiththetestprefix.
Theoutputprovidesdetailsindicatingthatthetestrunnerdiscoveredandexecutedfivetestsandallofthempassed.TheoutputdisplaysthemethodnameandtheclassnameforeachmethodintheInitialTestsclassthatstartedwiththetest_prefixandrepresentedatesttobeexecuted.
ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageusesthecodeanalysistoolsandthetracinghooksincludedinthePythonstandardlibrarytodeterminewhichlinesofcodeareexecutableandhavebeenexecuted.Thereportprovidesatablewiththefollowingcolumns:
Name:ThePythonmodulename.Stmts:ThecountofexecutablestatementsforthePythonmodule.Miss:Thenumberofexecutablestatementsmissed,thatis,theonesthatweren'texecuted.Cover:Thecoverageofexecutablestatementsexpressedasapercentage.
Wedefinitelyhaveaverylowcoverageforviews.pyandhelpers.pybasedonthemeasurementsshowninthereport.Infact,wejustwroteafewtestsrelatedtocategoriesandusers,andtherefore,itmakessensethatthecoverageisreallylowfortheviews.Wedidn'tcreatetestsrelatedtomessages.
Wecanrunthecoveragecommandwiththe-mcommand-lineoptiontodisplaythelinenumbersofthemissingstatementsinanewMissingcolumn:
coveragereport-m
Thecommandwillusetheinformationfromthelastexecutionandwilldisplaythemissingstatements.Thenextlinesshowasampleoutputthatcorrespondstothepreviousexecutionoftheunittests:
NameStmtsMissCoverMissing
---------------------------------------------------
app.py90100%
config.py11110%7-20
helpers.py231822%13-19,23-44
migrate.py990%7-19
models.py1012773%28-29,44,46,48,50,52,54,
73-75,79-86,103,127-137
run.py440%7-14
status.py56591%2,6,10,14,18
test_config.py120100%
tests\test_views.py960100%
views.py20410947%43-45,51-58,63-64,67,71-72,
83-87,92-94,97-124,127-135,140-147,150-181,194-195,198,205-206,209-
212,215-223,235-236,239,250-253
---------------------------------------------------
TOTAL52518365%
Now,runthefollowingcommandtogetannotatedHTMLlistingsdetailingmissedlines:
coveragehtml
Opentheindex.htmlHTMLfilegeneratedinthehtmlcovfolderwithyourWebbrowser.ThefollowingpictureshowsanexamplereportthatcoveragegeneratedinHTMLformat:
Clickortapviews.pyandtheWebbrowserwillrenderaWebpagethatdisplaysthestatementsthatwererun,themissingonesandtheexcluded,withdifferentcolors.Wecanclickortapontherun,missingandexcludedbuttonstoshoworhidethebackgroundcolorthatrepresentsthestatusforeachlineofcode.Bydefault,themissinglinesofcodewillbedisplayedwithapinkbackground.Thus,wemustwriteunitteststhattargettheselinesofcodetoimproveourtestcoverage:
ImprovingtestingcoverageNow,wewillwriteadditionalunitteststoimprovethetestingcoverage.Specifically,wewillwriteunittestsrelatedtomessagesandusers.Opentheexistingapi/tests/test_views.pyfileandinsertthefollowinglinesafterthelastline,withintheInitialTestsclass.WeneedanewimportstatementandwewilldeclarethenewPlayerTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_02folder:
defcreate_message(self,message,duration,category):
url=url_for('api.messagelistresource',_external=True)
data={'message':message,'duration':duration,'category':category}
response=self.test_client.post(
url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password),
data=json.dumps(data))
returnresponse
deftest_create_and_retrieve_message(self):
"""
Ensurewecancreateanewmessageandthenretrieveit
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_message_message='WelcometotheIoTworld'
new_message_category='Information'
post_response=self.create_message(new_message_message,15,
new_message_category)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Message.query.count(),1)
#Themessageshouldhavecreatedanewcatagory
self.assertEqual(Category.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
self.assertEqual(post_response_data['message'],new_message_message)
new_message_url=post_response_data['url']
get_response=self.test_client.get(
new_message_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['message'],new_message_message)
self.assertEqual(get_response_data['category']['name'],
new_message_category)
deftest_create_duplicated_message(self):
"""
EnsurewecannotcreateaduplicatedMessage
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_message_message='WelcometotheIoTworld'
new_message_category='Information'
post_response=self.create_message(new_message_message,15,
new_message_category)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Message.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
self.assertEqual(post_response_data['message'],new_message_message)
new_message_url=post_response_data['url']
get_response=self.test_client.get(
new_message_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['message'],new_message_message)
self.assertEqual(get_response_data['category']['name'],
new_message_category)
second_post_response=self.create_message(new_message_message,15,
new_message_category)
self.assertEqual(second_post_response.status_code,
status.HTTP_400_BAD_REQUEST)
self.assertEqual(Message.query.count(),1)
TheprecedingcodeaddsmanymethodstotheInitialTestsclass.Thecreate_messagemethodreceivesthedesiredmessage,duration,andcategory(categoryname)forthenewmessageasarguments.ThemethodbuildstheURLandthedatadictionarytocomposeandsendanHTTPPOSTmethod,createanewmessage,andreturntheresponsegeneratedbythisrequest.Manytestmethodswillcallthecreate_messagemethodtocreateamessageandthencomposeandsendotherHTTPrequeststotheAPI.
Theclassdeclaresthefollowingmethodswhosenamesstartwiththetest_prefix:
test_create_and_retrieve_message:TestswhetherwecancreateanewMessageandthenretrieveit.test_create_duplicated_message:Testswhethertheuniqueconstraintsdon'tmakeitpossibleforustocreatetwomessageswiththesamemessage.ThesecondtimewecomposeandsendanHTTPPOSTrequestwithaduplicatemessage,wemustreceiveanHTTP400BadRequeststatuscode(status.HTTP_400_BAD_REQUEST)andthetotalnumberofMessageobjectsretrievedfromthedatabasemustbe1.
Opentheexistingapi/tests/test_views.pyfileandinsertthefollowinglinesafterthelastline,withintheInitialTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_02folder:
deftest_retrieve_messages_list(self):
"""
Ensurewecanretrievethemessagespaginatedlist
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_message_message_1='WelcometotheIoTworld'
new_message_category_1='Information'
post_response=self.create_message(new_message_message_1,15,
new_message_category_1)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Message.query.count(),1)
new_message_message_2='Initializationoftheboardfailed'
new_message_category_2='Error'
post_response=self.create_message(new_message_message_2,10,
new_message_category_2)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Message.query.count(),2)
get_first_page_url=url_for('api.messagelistresource',_external=True)
get_first_page_response=self.test_client.get(
get_first_page_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_first_page_response_data=
json.loads(get_first_page_response.get_data(as_text=True))
self.assertEqual(get_first_page_response.status_code,
status.HTTP_200_OK)
self.assertEqual(get_first_page_response_data['count'],2)
self.assertIsNone(get_first_page_response_data['previous'])
self.assertIsNone(get_first_page_response_data['next'])
self.assertIsNotNone(get_first_page_response_data['results'])
self.assertEqual(len(get_first_page_response_data['results']),2)
self.assertEqual(get_first_page_response_data['results'][0]['message'],
new_message_message_1)
self.assertEqual(get_first_page_response_data['results'][1]['message'],
new_message_message_2)
get_second_page_url=url_for('api.messagelistresource',page=2)
get_second_page_response=self.test_client.get(
get_second_page_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_second_page_response_data=
json.loads(get_second_page_response.get_data(as_text=True))
self.assertEqual(get_second_page_response.status_code,
status.HTTP_200_OK)
self.assertIsNotNone(get_second_page_response_data['previous'])
self.assertEqual(get_second_page_response_data['previous'],
url_for('api.messagelistresource',page=1))
self.assertIsNone(get_second_page_response_data['next'])
self.assertIsNotNone(get_second_page_response_data['results'])
self.assertEqual(len(get_second_page_response_data['results']),0)
Thepreviouscodeaddedatest_retrieve_messages_listmethodtotheInitialTestsclass.Thismethodtestswhetherwecanretrievethepaginatedmessageslist.First,themethodcreatestwomessagesandthenitmakessurethattheretrievedlistincludesthetwocreatedmessagesinthefirstpage.Inaddition,themethodmakessurethatthesecondpagedoesn'tincludeanymessageandthatthevalueforthepreviouspageincludestheURLforthefirstpage.
Opentheexistingapi/tests/test_views.pyfileandinsertthefollowinglinesafterthelastline,withintheInitialTestsclass.Thecodefileforthesampleisincludedintherestful_python_chapter_08_02folder:
deftest_update_message(self):
"""
Ensurewecanupdateasinglefieldforanexistingmessage
"""
create_user_response=self.create_user(self.test_user_name,
self.test_user_password)
self.assertEqual(create_user_response.status_code,
status.HTTP_201_CREATED)
new_message_message_1='WelcometotheIoTworld'
new_message_category_1='Information'
post_response=self.create_message(new_message_message_1,30,
new_message_category_1)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(Message.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
new_message_url=post_response_data['url']
new_printed_times=1
new_printed_once=True
data={'printed_times':new_printed_times,'printed_once':
new_printed_once}
patch_response=self.test_client.patch(
new_message_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password),
data=json.dumps(data))
self.assertEqual(patch_response.status_code,status.HTTP_200_OK)
get_response=self.test_client.get(
new_message_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['printed_times'],new_printed_times)
self.assertEqual(get_response_data['printed_once'],new_printed_once)
deftest_create_and_retrieve_user(self):
"""
EnsurewecancreateanewUserandthenretrieveit
"""
new_user_name=self.test_user_name
new_user_password=self.test_user_password
post_response=self.create_user(new_user_name,new_user_password)
self.assertEqual(post_response.status_code,status.HTTP_201_CREATED)
self.assertEqual(User.query.count(),1)
post_response_data=json.loads(post_response.get_data(as_text=True))
self.assertEqual(post_response_data['name'],new_user_name)
new_user_url=post_response_data['url']
get_response=self.test_client.get(
new_user_url,
headers=self.get_authentication_headers(self.test_user_name,
self.test_user_password))
get_response_data=json.loads(get_response.get_data(as_text=True))
self.assertEqual(get_response.status_code,status.HTTP_200_OK)
self.assertEqual(get_response_data['name'],new_user_name)
ThepreviouscodeaddedthefollowingtwomethodstotheInitialTestsclass-test_update_message-testswhetherwecanupdatemorethanonefieldsforamessage,specifically,thevaluesfortheprinted_timesandprinted_oncefields.Thecodemakessurethatbothfieldshavebeenupdated.test_create_and_retrieve_user:TestswhetherwecancreateanewUserandthenretrieveit.
Wejustcodedafewtestsrelatedtomessagesandonetestrelatedtousersinordertoimprovetestcoverageandnoticetheimpactonthetestcoveragereport.
Now,runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing:
nose2-v--with-coverage
Thefollowinglinesshowthesampleoutput:
test_create_and_retrieve_category(test_views.InitialTests)...ok
test_create_and_retrieve_message(test_views.InitialTests)...ok
test_create_and_retrieve_user(test_views.InitialTests)...ok
test_create_duplicated_category(test_views.InitialTests)...ok
test_create_duplicated_message(test_views.InitialTests)...ok
test_request_without_authentication(test_views.InitialTests)...ok
test_retrieve_categories_list(test_views.InitialTests)...ok
test_retrieve_messages_list(test_views.InitialTests)...ok
test_update_category(test_views.InitialTests)...ok
test_update_message(test_views.InitialTests)...ok
------------------------------------------------------------------
Ran10testsin25.938s
OK
-----------coverage:platformwin32,python3.5.2-final-0-------
NameStmtsMissCover
-----------------------------------------
app.py90100%
config.py11110%
helpers.py23196%
migrate.py990%
models.py1011189%
run.py440%
status.py56591%
test_config.py160100%
tests\test_views.py2030100%
views.py2046668%
-----------------------------------------
TOTAL63610783%
Theoutputprovideddetailsindicatingthatthetestrunnerexecuted10testsandallofthempassed.ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageincreasedtheCoverpercentageoftheviews.pymodulefrom47%inthepreviousrunto68%.Inaddition,thepercentageofthehelpers.pymoduleincreasedfrom22%to96%becausewe
wroteteststhatusedpagination.Thenewadditionaltestswewroteexecutedadditionalcodeindifferentmodules,andtherefore,thereisanimpactinthecoveragereport.
Tip
Wejustcreatedafewunitteststounderstandhowwecancodethem.However,ofcourse,itwouldbenecessarytowritemoreteststoprovideanappropriatecoverageofallthefeaturedandexecutionscenariosincludedintheAPI.
UnderstandingstrategiesfordeploymentsandscalabilityFlaskisalightweightmicroframeworkfortheWeb.However,ashappenswithDjango,oneofthebiggestdrawbacksrelatedtoFlaskandFlask-RESTfulisthateachHTTPrequestisblocking.Thus,whenevertheFlaskserverreceivesanHTTPrequest,itdoesn'tstartworkingonanyotherHTTPrequestsintheincomingqueueuntiltheserversendstheresponseforthefirstHTTPrequestitreceived.
WeusedFlasktodevelopaRESTfulWebService.TheykeyadvantageofthesekindofWebServicesisthattheyarestateless,thatis,theyshouldn'tkeepaclientstateonanyserver.OurAPIisagoodexampleofastatelessRESTfulWebServicewithFlaskandFlaskRESTful.Thus,wecanmaketheAPIrunonasmanyserversasnecessarytoachieveourscalabilitygoals.Obviously,wemusttakeintoaccountthatwecaneasilytransformthedatabaseserverinourscalabilitybottleneck.
Tip
Nowadays,wehaveahugenumberofcloud-basedalternativestodeployaRESTfulWebServicethatusesFlaskandFlask-RESTfulandmakeitextremelyscalable.
WealwayshavetomakesurethatweprofiletheAPIandthedatabasebeforewedeploythefirstversionofourAPI.Itisveryimportanttomakesurethatthegeneratedqueriesrunproperlyontheunderlyingdatabaseandthatthemostpopularqueriesdonotendupinsequentialscans.Itisusuallynecessarytoaddtheappropriateindexestothetablesinthedatabase.
WehavebeenusingbasicHTTPauthentication.Wecanimproveitwithatoken-basedauthentication.WemustmakesurethattheAPIrunsunderHTTPSinproductionenvironments.Inaddition,wemustmakesurethatwechangethefollowinglineintheapi/config.pyfile:
DEBUG=True
Wemustalwaysturnoffdebugmodeinproduction,andtherefore,wemustreplacethepreviouslinewiththefollowingone:
DEBUG=False
Tip
Itisconvenienttouseadifferentconfigurationfileforproduction.However,anotherapproachthatisbecomingextremelypopular,especiallyforcloud-nativeapplications,istostoreconfigurationintheenvironment.Ifwewanttodeploycloud-nativeRESTfulWebServicesandfollowtheguidelinesestablishedinthetwelve-factorApp,weshouldstore
configintheenvironment.
Eachplatformincludesdetailedinstructionstodeployourapplication.Allofthemwillrequireustogeneratetherequirements.txtfilethatliststheapplicationdependenciestogetherwiththeirversions.Thisway,theplatformswillbeabletoinstallallthenecessarydependencieslistedinthefile.
Runthefollowingpipfreezetogeneratetherequirements.txtfile.
pipfreeze>requirements.txt
Thefollowinglinesshowthecontentsofasamplegeneratedrequirements.txtfile.However,bearinmindthatmanypackagesincreasetheirversionnumberquicklyandyoumightseedifferentversionsinyourconfiguration:
alembic==0.8.8
aniso8601==1.1.0
click==6.6
cov-core==1.15.0
coverage==4.2
Flask==0.11.1
Flask-HTTPAuth==3.2.1
flask-marshmallow==0.7.0
Flask-Migrate==2.0.0
Flask-RESTful==0.3.5
Flask-Script==2.0.5
Flask-SQLAlchemy==2.1
itsdangerous==0.24
Jinja2==2.8
Mako==1.0.4
MarkupSafe==0.23
marshmallow==2.10.2
marshmallow-sqlalchemy==0.10.0
nose2==0.6.5
passlib==1.6.5
psycopg2==2.6.2
python-dateutil==2.5.3
python-editor==1.0.1
pytz==2016.6.1
six==1.10.0
SQLAlchemy==1.0.15
Werkzeug==0.11.11
Testyourknowledge1. Bydefault,nose2looksformoduleswhosenamesstartwiththefollowingprefix:
1. test2. run3. unittest
2. Bydefault,nose2loadstestsfromallthesubclassesofthefollowingclass:1. unittest.Test2. unittest.TestCase3. unittest.RunTest
3. ThesetUpmethodinasubclassofunittest.TestCase:1. Isexecutedbeforeeachtestmethodruns.2. Isexecutedonlyoncebeforeallthetestsstarttheirexecution.3. Isexecutedonlyonceafterallthetestsfinishtheirexecution.
4. ThetearDownmethodinasubclassofunittest.TestCase:1. Isexecutedaftereachtestmethodruns.2. Isexecutedbeforeeachtestmethodruns.3. Isexecutedafteratestmethodonlywhenitfails.
5. Ifwedeclareaget_accept_content_type_headersmethodwithinasubclassofunittest.TestCase,bydefault,nose2:1. Willloadthismethodasatest.2. WillloadthismethodasthesetUpmethodforeachtest.3. Won'tloadthismethodasatest.
SummaryInthischapter,wesetupatestingenvironment.Weinstallednose2tomakeiteasytodiscoverandexecuteunittests,andwecreatedanewdatabasetobeusedfortesting.Wewroteafirstroundofunittests,measuredtestcoverage,andthenwewroteadditionalunitteststoimprovetestcoverage.Finally,weunderstoodmanyconsiderationsfordeploymentandscalability.
NowthatwehavebuiltacomplexAPIwithFlaskcombinedwithFlaskRESTful,andwetestedit,wewillmovetoanotherpopularPythonWebframework,Tornado,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter9.DevelopingRESTfulAPIswithTornadoInthischapter,wewillworkwithTornadotocreateaRESTfulWebAPIandstartworkingwiththislightweightWebframework.Wewillcoverthefollowingtopics:
DesigningaRESTfulAPItointeractwithslowsensorsandactuatorsUnderstandingthetasksperformedbyeachHTTPmethodSettingupavirtualenvironmentwithTornadoDeclaringstatuscodesfortheresponsesCreatingtheclassesthatrepresentadroneWritingrequesthandlersMappingURLpatternstorequesthandlersMakingHTTPrequeststotheTornadoAPIWorkingwithcommand-linetools-curlandHTTPieWorkingwithGUItools-Postmanandothers
DesigningaRESTfulAPItointeractwithslowsensorsandactuatorsImaginethatwehavetocreateaRESTfulAPItocontroladrone,alsoknownasanUnmannedAerialVehicle(UAV).ThedroneisanIoTdevicethatinteractswithmanysensorsandactuators,includingdigitalelectronicspeedcontrollerslinkedtoengines,propellers,andservomotors.
TheIoTdevicehaslimitedresources,andtherefore,wehavetousealightweightWebframework.OurAPIdoesn'tneedtointeractwithadatabase.Wedon'tneedaheavyweightWebframeworklikeDjango,andwewanttobeabletoprocessmanyrequestswithoutblockingtheWebserver.WeneedtheWebservertoprovideuswithgoodscalabilitywhileconsuminglimitedresources.Thus,ourchoiceistouseTornado,theopensourceversionofFriendFeed'sWebserver.
TheIoTdeviceiscapableofrunningPython3.5,Tornado,andotherPythonpackages.TornadoisaPythonWebframeworkandanasynchronousnetworkinglibrarythatprovidesexcellentscalabilityduetoitsnon-blockingnetworkI/O.Inaddition,TornadowillallowustoeasilyandquicklybuildalightweightRESTfulAPI.
WehavechosenTornadobecauseitismorelightweightthanDjangoanditmakesiteasyforustocreateanAPIthattakesadvantageofthenon-blockingnetworkI/O.Wedon'tneedtouseanORM,andwewanttostartrunningtheRESTfulAPIontheIoTdeviceassoonaspossibletoallowalltheteamstointeractwithit.
WewillinteractwithalibrarythatallowsustoruntheslowI/OoperationsthatinteractwiththesensorsandactuatorswithanexecutionthathappensoutsidetheGlobalInterpreterLock(GIL).Thus,wewillbeabletotakeadvantageofthenon-blockingfeatureinTornadowhenarequestneedstoexecuteanyoftheseslowI/Ooperations.InourfirstversionoftheAPI,wewillworkwithasynchronousexecution,andtherefore,whenanHTTPrequesttoourAPIrequiresrunningaslowI/Ooperation,wewillblocktherequestprocessingqueueuntiltheslowI/Ooperationwitheitherasensororanactuatorprovidesaresponse.WewillexecutetheI/OoperationwithasynchronousexecutionandTornadowon'tbeabletocontinueprocessingotherincomingHTTPrequestsuntilaresponseissenttotheHTTPrequest.
Then,wewillcreateasecondversionofourAPIthatwilltakeadvantageofthenon-blockingfeaturesincludedinTornado,incombinationwithasynchronousoperations.Inthesecondversion,whenanHTTPrequesttoourAPIrequiresrunningaslowI/Ooperation,wewon'tblocktherequestprocessingqueueuntiltheslowI/Ooperationwitheitherasensororanactuatorprovidesaresponse.WewillexecutetheI/Ooperationwithanasynchronousexecution,andTornadowillbeabletocontinueprocessingotherincomingHTTPrequests.
Tip
Wewillkeepourexamplesimpleandwewon'tusealibrarytointeractwithsensorsandactuators.Wewilljustprintinformationabouttheoperationsthatwillbeperformedbythesesensorsandactuators.However,inoursecondversionoftheAPI,wewillwriteourcodetomakeasynchronouscallsinordertounderstandtheadvantagesofthenon-blockingfeaturesinTornado.Wewilluseasimplifiedsetofsensorsandactuators—bearinmindthatdronesusuallyhavemoresensorsandactuators.OurgoalistolearnhowtoworkwithTornadotobuildaRESTfulAPI;wedon'twanttobecomeexpertsinbuildingdrones.
EachofthefollowingsensorsandactuatorswillbearesourceinourRESTfulAPI:
Ahexacopter,thatis,a6-rotorhelicopterAnaltimeter(altitudesensor)AblueLED(Light-EmittingDiode)AwhiteLED
ThefollowingtableshowstheHTTPverbs,thescope,andthesemanticsforthemethodsthatourfirstversionoftheAPImustsupport.EachmethodiscomposedbyanHTTPverbandascopeandallthemethodshaveawell-definedmeaningforallsensorsandactuators.InourAPI,eachsensororactuatorhasitsownuniqueURL:
HTTPverb Scope Semantics
GET Hexacopter Retrievethecurrenthexacopter'smotorspeedinRPMsanditsstatus(turnedonoroff)
PATCH Hexacopter Setthecurrenthexacopter'smotorspeedinRPMs
GET LED RetrievethebrightnesslevelforasingleLED
PATCH LED UpdatethebrightnesslevelforasingleLED
GET Altimeter Retrievethecurrentaltitudeinfeet
UnderstandingthetasksperformedbyeachHTTPmethodLet'sconsiderthathttp://localhost:8888/hexacopters/1istheURLthatidentifiesthehexacopterforourdrone.
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(PATCH)andrequestURL(http://localhost:8888/hexacopters/1)tosetthehexacopter'smotorspeedinRPMsanditsstatus.Inaddition,wehavetoprovidetheJSONkey-valuepairswiththenecessaryfieldnameandthevaluetospecifythedesiredspeed.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefield,makesurethatitisavalidspeedandmakethenecessarycallstoadjustthespeedwithanasynchronousexecution.Afterthespeedforthehexacopterisset,theserverwillreturna200OKstatuscodeandaJSONbodywiththerecentlyupdatedhexacoptervaluesserializedtoJSON:
PATCHhttp://localhost:8888/hexacopters/1
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:8888/hexacopter/1)toretrievethecurrentvaluesforthehexacopter.Theserverwillmakethenecessarycallstoretrievethestatusandthespeedforthehexacopterwithanasynchronousexecution.Asaresultoftherequest,theserverwillreturna200OKstatuscodeandaJSONbodywiththeserializedkey-valuepairsthatspecifythestatusandspeedforthehexacopter.Ifanumberdifferentthan1isspecified,theserverwillreturnjusta404NotFoundstatus:
GEThttp://localhost:8888/hexacopters/1
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(PATCH)andrequestURL(http://localhost:8888/led/{id})tosetthebrightnesslevelforaspecificLEDwhoseidmatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Forexample,ifweusetherequestURLhttp://localhost:8888/led/1,theserverwillsetthebrightnesslevelfortheledwhoseidmatches1.Inaddition,wehavetoprovidetheJSONkey-valuepairswiththenecessaryfieldnameandthevaluetospecifythedesiredbrightnesslevel.Asaresultoftherequest,theserverwillvalidatetheprovidedvaluesforthefield,makesurethatitisavalidbrightnesslevelandmakethenecessarycallstoadjustthebrightnesslevelwithanasynchronousexecution.AfterthebrightnesslevelfortheLEDisset,theserverwillreturna200OKstatuscodeandaJSONbodywiththerecentlyupdatedLEDvaluesserializedtoJSON:
PATCHhttp://localhost:8888/led/{id}
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:8888/led/{id})toretrievethecurrentvaluesfortheLEDwhoseidmatchesthespecifiednumericvalueintheplacewhere{id}iswritten.Forexample,
ifweusetherequestURLhttp://localhost:8888/led/1,theserverwillretrievetheLEDwhoseidmatches1.TheserverwillmakethenecessarycallstoretrievethevaluesfortheLEDwithanasynchronousexecution.Asaresultoftherequest,theserverwillreturna200OKstatuscodeandaJSONbodywiththeserializedkey-valuepairsthatspecifythevaluesfortheLED.IfnoLEDmatchesthespecifiedid,theserverwillreturnjusta404NotFoundstatus:
GEThttp://localhost:8888/led/{id}
WehavetocomposeandsendanHTTPrequestwiththefollowingHTTPverb(GET)andrequestURL(http://localhost:8888/altimeter/1)toretrievethecurrentvaluesforthealtimeter.Theserverwillmakethenecessarycallstoretrievethevaluesforthealtimeterwithanasynchronousexecution.Asaresultoftherequest,theserverwillreturna200OKstatuscodeandaJSONbodywiththeserializedkey-valuepairsthatspecifythevaluesforthealtimeter.Ifanumberdifferentthan1isspecified,theserverwillreturnjusta404NotFoundstatus:
GEThttp://localhost:8888/altimeter/1
SettingupavirtualenvironmentwithTornadoInChapter1,DevelopingRESTfulAPIswithDjango,welearnedthat,throughoutthisbook,weweregoingtoworkwiththelightweightvirtualenvironmentsintroducedinPython3.3andimprovedinPython3.4.Now,wewillfollowmanystepscreateanewlightweightvirtualenvironmenttoworkwithTornado.ItishighlyrecommendedtoreadChapter1,DevelopingRESTfulAPIswithDjango,incaseyoudon'thaveexperiencewithlightweightvirtualenvironmentsinPython.Thechapterincludesallthedetailedexplanationsabouttheeffectsofthestepswearegoingtofollow.
First,wehavetoselectthetargetfolderordirectoryforourvirtualenvironment.ThefollowingisthepathwewilluseintheexampleformacOSandLinux.ThetargetfolderforthevirtualenvironmentwillbethePythonREST/Tornado01folderwithinourhomedirectory.Forexample,ifourhomedirectoryinmacOSorLinuxis/Users/gaston,thevirtualenvironmentwillbecreatedwithin/Users/gaston/PythonREST/Tornado01.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand:
~/PythonREST/Tornado01
WewillusethefollowingpathintheexampleforWindows.ThetargetfolderforthevirtualenvironmentwillbethePythonREST\Tornado01folderwithinouruserprofilefolder.Forexample,ifouruserprofilefolderisC:\Users\Gaston,thevirtualenvironmentwillbecreatedwithinC:\Users\gaston\PythonREST\Tornado01.Youcanreplacethespecifiedpathwithyourdesiredpathineachcommand:
%USERPROFILE%\PythonREST\Tornado01
OpenaTerminalinmacOSorLinuxandexecutethefollowingcommandtocreateavirtualenvironment:
python3-mvenv~/PythonREST/Tornado01
InWindows,executethefollowingcommandtocreateavirtualenvironment:
python-mvenv%USERPROFILE%\PythonREST\Tornado01
Theprecedingcommanddoesn'tproduceanyoutput.Nowthatwehavecreatedavirtualenvironment,wewillrunaplatform-specificscripttoactivateit.Afterweactivatethevirtualenvironment,wewillinstallpackagesthatwillonlybeavailableinthisvirtualenvironment.
IfyourTerminalisconfiguredtousethebashshellinmacOSorLinux,runthefollowingcommandtoactivatethevirtualenvironment.Thecommandalsoworksforthezshshell:
source~/PythonREST/Torando01/bin/activate
IfyourTerminalisconfiguredtouseeitherthecshortcshshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Torando01/bin/activate.csh
IfyourTerminalisconfiguredtouseeitherthefishshell,runthefollowingcommandtoactivatethevirtualenvironment:
source~/PythonREST/Tornado01/bin/activate.fish
InWindows,youcanruneitherabatchfileintheCommandPromptoraWindowsPowerShellscripttoactivatethevirtualenvironment.Ifyoupreferthecommandprompt,runthefollowingcommandintheWindowscommandlinetoactivatethevirtualenvironment:
%USERPROFILE%\PythonREST\Tornado01\Scripts\activate.bat
IfyouprefertheWindowsPowerShell,launchitandrunthefollowingcommandstoactivatethevirtualenvironment.However,noticethatyoushouldhavescriptsexecutionenabledinWindowsPowerShelltobeabletorunthescript:
cd$env:USERPROFILE
PythonREST\Tornado01\Scripts\Activate.ps1
Afteryouactivatethevirtualenvironment,theCommandPromptwilldisplaythevirtualenvironmentrootfoldernameenclosedinparenthesesasaprefixofthedefaultprompttoremindusthatweareworkinginthevirtualenvironment.Inthiscase,wewillsee(Tornado01)asaprefixfortheCommandPromptbecausetherootfolderfortheactivatedvirtualenvironmentisTornado01.
Wehavecreatedandactivatedavirtualenvironment.ItistimetorunmanycommandsthatwillbethesameforeithermacOS,Linux,orWindows.Now,wemustrunthefollowingcommandtoinstallTornadowithpip:
pipinstalltornado
Thelastlinesfortheoutputwillindicateallthepackagesthathavebeensuccessfullyinstalled,includingtornado:
Collectingtornado
Downloadingtornado-4.4.1.tar.gz(456kB)
Installingcollectedpackages:tornado
Runningsetup.pyinstallfortornado
Successfullyinstalledtornado-4.4.1
DeclaringstatuscodesfortheresponsesTornadoallowsustogenerateresponseswithanystatuscodethatisincludedinthehttp.HTTPStatusdictionary.Wemightusethisdictionarytoreturneasytounderstanddescriptionsasthestatuscodes,suchasHTTPStatus.OKandHTTPStatus.NOT_FOUNDafterimportingtheHTTPStatusdictionaryfromthehttpmodule.Thesenamesareeasytounderstandbuttheydon'tincludethestatuscodenumberintheirdescription.
Wehavebeenworkingwithmanydifferentframeworksandmicro-frameworksthroughoutthebook,andtherefore,wewillborrowthecodethatdeclaresveryusefulfunctionsandvariablesrelatedtoHTTPstatuscodesfromthestatus.pyfileincludedinDjangoRESTFramework,thatis,theframeworkwehavebeenusinginthefirstchapters.ThemainadvantageofusingthesevariablesfortheHTTPstatuscodesisthattheirnamesincludeboththenumberandthedescription.Whenwereadthecode,wewillunderstandthestatuscodenumberandtheirmeaning.Forexample,insteadofusingHTTPStatus.OK,wewillusestatus.HTTP_200_OK.
Createanewstatus.pyfilewithintherootfolderfortherecentlycreatedvirtualenvironment.ThefollowinglinesshowthecodethatdeclaresfunctionsandvariableswithdescriptiveHTTPstatuscodesinthestatus.pyfile,borrowedfromtherest_framework.statusmodule.Wedon'twanttoreinventthewheelandthemoduleprovideseverythingweneedtoworkwithHTTPstatuscodesinourTornado-basedAPI.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
defis_informational(code):
returncode>=100andcode<=199
defis_success(code):
returncode>=200andcode<=299
defis_redirect(code):
returncode>=300andcode<=399
defis_client_error(code):
returncode>=400andcode<=499
defis_server_error(code):
returncode>=500andcode<=599
HTTP_100_CONTINUE=100
HTTP_101_SWITCHING_PROTOCOLS=101
HTTP_200_OK=200
HTTP_201_CREATED=201
HTTP_202_ACCEPTED=202
HTTP_203_NON_AUTHORITATIVE_INFORMATION=203
HTTP_204_NO_CONTENT=204
HTTP_205_RESET_CONTENT=205
HTTP_206_PARTIAL_CONTENT=206
HTTP_300_MULTIPLE_CHOICES=300
HTTP_301_MOVED_PERMANENTLY=301
HTTP_302_FOUND=302
HTTP_303_SEE_OTHER=303
HTTP_304_NOT_MODIFIED=304
HTTP_305_USE_PROXY=305
HTTP_306_RESERVED=306
HTTP_307_TEMPORARY_REDIRECT=307
HTTP_400_BAD_REQUEST=400
HTTP_401_UNAUTHORIZED=401
HTTP_402_PAYMENT_REQUIRED=402
HTTP_403_FORBIDDEN=403
HTTP_404_NOT_FOUND=404
HTTP_405_METHOD_NOT_ALLOWED=405
HTTP_406_NOT_ACCEPTABLE=406
HTTP_407_PROXY_AUTHENTICATION_REQUIRED=407
HTTP_408_REQUEST_TIMEOUT=408
HTTP_409_CONFLICT=409
HTTP_410_GONE=410
HTTP_411_LENGTH_REQUIRED=411
HTTP_412_PRECONDITION_FAILED=412
HTTP_413_REQUEST_ENTITY_TOO_LARGE=413
HTTP_414_REQUEST_URI_TOO_LONG=414
HTTP_415_UNSUPPORTED_MEDIA_TYPE=415
HTTP_416_REQUESTED_RANGE_NOT_SATISFIABLE=416
HTTP_417_EXPECTATION_FAILED=417
HTTP_428_PRECONDITION_REQUIRED=428
HTTP_429_TOO_MANY_REQUESTS=429
HTTP_431_REQUEST_HEADER_FIELDS_TOO_LARGE=431
HTTP_451_UNAVAILABLE_FOR_LEGAL_REASONS=451
HTTP_500_INTERNAL_SERVER_ERROR=500
HTTP_501_NOT_IMPLEMENTED=501
HTTP_502_BAD_GATEWAY=502
HTTP_503_SERVICE_UNAVAILABLE=503
HTTP_504_GATEWAY_TIMEOUT=504
HTTP_505_HTTP_VERSION_NOT_SUPPORTED=505
HTTP_511_NETWORK_AUTHENTICATION_REQUIRED=511
ThecodedeclaresfivefunctionsthatreceivetheHTTPstatuscodeinthecodeargumentanddeterminetowhichofthefollowingcategoriesthestatuscodebelongsto:informational,success,redirect,andclienterrororservererrorcategories.Wewillusethepreviousvariableswhenwehavetoreturnaspecificstatuscode.Forexample,incasewehavetoreturna404NotFoundstatuscode,wewillreturnstatus.HTTP_404_NOT_FOUND,insteadofjust404orHTTPStatus.NOT_FOUND.
CreatingtheclassesthatrepresentadroneWewillcreateasmanyclassesaswewillusetorepresentthedifferentcomponentsofadrone.Inareal-lifeexample,theseclasseswillinteractwithalibrarythatinteractswithsensorsandactuators.Inordertokeepourexamplesimple,wewillmakecallstotime.sleeptosimulateinteractionsthattakesometimetosetorgetvaluestoandfromsensorsandactuators.
First,wewillcreateaHexacopterclassthatwewillusetorepresentthehexacopterandaHexacopterStatusclassthatwewillusetostorestatusdataforthehexacopter.Createanewdrone.pyfile.ThefollowinglinesshowsallthenecessaryimportsfortheclassesthatwewillcreateandthecodethatdeclarestheHexacopterandHexacopterStatusclassesinthedrone.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
fromrandomimportrandint
fromtimeimportsleep
classHexacopterStatus:
def__init__(self,motor_speed,turned_on):
self.motor_speed=motor_speed
self.turned_on=turned_on
classHexacopter:
MIN_SPEED=0
MAX_SPEED=1000
def__init__(self):
self.motor_speed=self.__class__.MIN_SPEED
self.turned_on=False
defget_motor_speed(self):
returnself.motor_speed
defset_motor_speed(self,motor_speed):
ifmotor_speed<self.__class__.MIN_SPEED:
raiseValueError('Theminimumspeedis
{0}'.format(self.__class__.MIN_SPEED))
ifmotor_speed>self.__class__.MAX_SPEED:
raiseValueError('Themaximumspeedis
{0}'.format(self.__class__.MAX_SPEED))
self.motor_speed=motor_speed
self.turned_on=(self.motor_speedisnot0)
sleep(2)
returnHexacopterStatus(self.get_motor_speed(),self.is_turned_on())
defis_turned_on(self):
returnself.turned_on
defget_hexacopter_status(self):
sleep(3)
returnHexacopterStatus(self.get_motor_speed(),self.is_turned_on())
TheHexacopterStatusclassjustdeclaresaconstructor,thatis,the__init__method.Thismethodreceivesmanyargumentsandusesthemtoinitializetheattributeswiththesamenames:motor_speedandturned_on.
TheHexacopterclassdeclarestwoclassattributesthatspecifytheminimumandmaximumspeedvalues:MIN_SPEEDandMAX_SPEED.Theconstructor,thatis,the__init__method,initializesthemotor_speedattributewiththeMIN_SPEEDvalueandsetstheturned_onattributetoFalse.
Theget_motor_speedmethodreturnsthevalueofthemotor_speedattribute.Theset_motor_speedmethodcheckswhetherthevalueforthemotor_speedargumentisinthevalidrange.Incasethevalidationfails,themethodraisesaValueErrorexception.Otherwise,themethodsetsthevalueofthemotor_speedattributewiththereceivedvalueandsetsthevaluefortheturned_onattributetoTrueifthemotor_speedisgreaterthan0.Finally,themethodcallssleeptosimulateittakestwosecondstoretrievethehexacopterstatusandthenreturnsaHexacopterStatusinstanceinitializedwiththemotor_speedandturned_onattributevalues,retrievedthroughspecificmethods.
Theget_hexacopter_statusmethodcallssleeptosimulateittakesthreesecondstoretrievethehexacopterstatusandthenreturnsaHexacopterStatusinstanceinitializedwiththemotor_speedandturned_onattributevalues.
Now,wewillcreateaLightEmittingDiodeclassthatwewillusetorepresenteachLED.Openthepreviouslycreateddrone.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
classLightEmittingDiode:
MIN_BRIGHTNESS_LEVEL=0
MAX_BRIGHTNESS_LEVEL=255
def__init__(self,identifier,description):
self.identifier=identifier
self.description=description
self.brightness_level=self.__class__.MIN_BRIGHTNESS_LEVEL
defget_brightness_level(self):
sleep(1)
returnself.brightness_level
defset_brightness_level(self,brightness_level):
ifbrightness_level<self.__class__.MIN_BRIGHTNESS_LEVEL:
raiseValueError('Theminimumbrightnesslevelis
{0}'.format(self.__class__.MIN_BRIGHTNESS_LEVEL))
ifbrightness_level>self.__class__.MAX_BRIGHTNESS_LEVEL:
raiseValueError('Themaximumbrightnesslevelis
{0}'.format(self.__class__.MAX_BRIGHTNESS_LEVEL))
sleep(2)
self.brightness_level=brightness_level
TheLightEmittingDiodeclassdeclarestwoclassattributesthatspecifytheminimumandmaximumbrightnesslevelvalues:MIN_BRIGHTNESS_LEVELandMAX_BRIGHTNESS_LEVEL.Theconstructor,thatis,the__init__method,initializesthebrightness_levelattributewiththeMIN_BRIGHTNESS_LEVELandtheidanddescriptionattributeswiththevaluesreceivedintheargumentswiththesamenames.
Theget_brightness_levelmethodcallssleeptosimulate,ittakes1secondtoretrievethebrightnesslevelforthewiredLEDandthenreturnsthevalueofthebrightness_levelattribute.
Theset_brightness_levelmethodcheckswhetherthevalueforthebrightness_levelargumentisinthevalidrange.Incasethevalidationfails,themethodraisesaValueErrorexception.Otherwise,themethodcallssleeptosimulateittakestwosecondstosetthenewbrightnesslevelandfinallysetsthevalueofthebrightness_levelattributewiththereceivedvalue.
Now,wewillcreateanAltimeterclassthatwewillusetorepresentthealtimeter.Openthepreviouslycreateddrone.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
classAltimeter:
defget_altitude(self):
sleep(1)
returnrandint(0,3000)
TheAltimeterclassdeclaresaget_altitudemethodthatcallssleeptosimulateittakesonesecondtoretrievethealtitudefromthealtimeterandfinallygeneratesarandomintegerfrom0to3000(inclusive)andreturnsit.
Finally,wewillcreateaDroneclassthatwewillusetorepresentthedronewithitssensorsandactuators.Openthepreviouslycreateddrone.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder
classDrone:
def__init__(self):
self.hexacopter=Hexacopter()
self.altimeter=Altimeter()
self.blue_led=LightEmittingDiode(1,'BlueLED')
self.white_led=LightEmittingDiode(2,'WhiteLED')
self.leds={
self.blue_led.identifier:self.blue_led,
self.white_led.identifier:self.white_led
}
TheDroneclassjustdeclaresaconstructor,thatis,the__init__methodthatcreatesinstancesofthepreviouslydeclaredclassesthatrepresentthedifferentcomponentsforthedrone.Theledsattributesavesadictionarythathasakey-valuepairforeachLightEmittingDiode
instancewithitsidanditsinstance.
WritingrequesthandlersThemainbuildingblocksforaRESTfulAPIintornadoaresubclassesofthetornado.web.RequestHandlerclass,thatis,thebaseclassforHTTPrequesthandlersinTornado.WejustneedtocreateasubclassofthisclassanddeclarethemethodsforeachsupportedHTTPverb.WehavetooverridethemethodstohandleHTTPrequests.Then,wehavetomaptheURLpatternstoeachsubclassoftornado.web.RequestHandlerinthetornado.web.ApplicationinstancethatrepresentstheTornadoWebapplication.
First,wewillcreateaHexacopterHandlerclassthatwewillusetohandlerequestsforthehexacopterresource.Createanewapi.pyfile.ThefollowinglinesshowallthenecessaryimportsfortheclassesthatwewillcreateandthecodethatdeclarestheHexacopterHandlerclassinthedrone.pyfile.Enterthenextlinesinthenewapi.pyfile.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
importstatus
fromdatetimeimportdate
fromtornadoimportweb,escape,ioloop,httpclient,gen
fromdroneimportAltimeter,Drone,Hexacopter,LightEmittingDiode
drone=Drone()
classHexacopterHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET","PATCH")
HEXACOPTER_ID=1
defget(self,id):
ifint(id)isnotself.__class__.HEXACOPTER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
return
print("I'vestartedretrievinghexacopter'sstatus")
hexacopter_status=drone.hexacopter.get_hexacopter_status()
print("I'vefinishedretrievinghexacopter'sstatus")
response={
'speed':hexacopter_status.motor_speed,
'turned_on':hexacopter_status.turned_on,
}
self.set_status(status.HTTP_200_OK)
self.write(response)
defpatch(self,id):
ifint(id)isnotself.__class__.HEXACOPTER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
return
request_data=escape.json_decode(self.request.body)
if('motor_speed'notinrequest_data.keys())or\
(request_data['motor_speed']isNone):
self.set_status(status.HTTP_400_BAD_REQUEST)
return
try:
motor_speed=int(request_data['motor_speed'])
print("I'vestartedsettingthehexacopter'smotorspeed")
hexacopter_status=drone.hexacopter.set_motor_speed(motor_speed)
print("I'vefinishedsettingthehexacopter'smotorspeed")
response={
'speed':hexacopter_status.motor_speed,
'turned_on':hexacopter_status.turned_on,
}
self.set_status(status.HTTP_200_OK)
self.write(response)
exceptValueErrorase:
print("I'vefailedsettingthehexacopter'smotorspeed")
self.set_status(status.HTTP_400_BAD_REQUEST)
response={
'error':e.args[0]
}
self.write(response)
TheHexacopterHandlerclassisasubclassoftornado.web.RequestHandleranddeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestonthisHTTPhandler:
get:Thismethodreceivestheidofthehexacopterwhosestatushastoberetrievedintheidargument.Ifthereceivediddoesn'tmatchthevalueoftheHEXACOPTER_IDclassattribute,thecodecallstheself.set_statusmethodwithstatus.HTTP_404_NOT_FOUNDasanargumenttosetthestatuscodefortheresponsetoHTTP404NotFound.Otherwise,thecodeprintsamessageindicatingthatitstartedretrievingthehexacopter'sstatusandcallsthedrone.hexacopter.get_hexacopter_statusmethodwithasynchronousexecutionandsavestheresultinthehexacopter_statusvariable.Then,thecodewritesamessageindicatingitfinishedretrievingthestatusandgeneratesaresponsedictionarywiththe'speed'and'turned_on'keysandtheirvalues.Finally,thecodecallstheself.set_statusmethodwithstatus.HTTP_200_OKasanargumenttosetthestatuscodefortheresponsetoHTTP200OKandcallstheself.writemethodwiththeresponsedictionaryasanargument.Becauseresponseisadictionary,TornadoautomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Typeheadertoapplication/json.patch:Thismethodreceivestheidofthehexacopterthathastobeupdatedorpatchedintheidargument.Asithappenedinthepreviouslyexplainedgetmethod,thecodereturnsanHTTP404NotFoundincasethereceivediddoesn'tmatchthevalueoftheHEXACOPTER_IDclassattribute.Otherwise,thecodecallsthetornado.escape.json_decodemethodwithself.request.bodyasanargumenttogeneratePythonobjectsfortheJSONstringoftherequestbodyandsavesthegenerateddictionaryintherequest_datavariable.Ifthedictionarydoesn'tincludeakeynamed'motor_speed',thecodereturnsanHTTP400BadRequeststatuscode.Incasethereisakey,thecodeprintsamessageindicatingthatitstartedsettingthehexacopter'sspeed,callsthedrone.hexacopter.set_motor_speedmethodwithasynchronousexecutionandsavestheresultinthehexacopter_statusvariable.Ifthevaluespecifiedforthemotorspeedisnotvalid,aValueErrorexceptionwillbecaughtandthecodewillreturnan
HTTP400BadRequeststatuscodeandthevalidationerrormessagesastheresponsebody.Otherwise,thecodewritesamessageindicatingitfinishedsettingthemotorspeedandgeneratesaresponsedictionarywiththe'speed'and'turned_on'keysandtheirvalues.Finally,thecodecallstheself.set_statusmethodwithstatus.HTTP_200_OKasanargumenttosetthestatuscodefortheresponsetoHTTP200OKandcallstheself.writemethodwiththeresponsedictionaryasanargument.Sinceresponseisadictionary,TornadoautomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Typeheadertoapplication/json.
TheclassoverridestheSUPPORTED_METHODSclassvariablewithatuplethatindicatestheclassjustsupportstheGETandPATCHmethods.Thisway,incasethehandlerisrequestedamethodthatisn'tincludedintheSUPPORTED_METHODStuple,theserverwillautomaticallyreturna405MethodNotAllowedstatuscode.
Now,wewillcreateaLedHandlerclassthatwewillusetorepresenttheLEDresources.Openthepreviouslycreatedapi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
classLedHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET","PATCH")
defget(self,id):
int_id=int(id)
ifint_idnotindrone.leds.keys():
self.set_status(status.HTTP_404_NOT_FOUND)
return
led=drone.leds[int_id]
print("I'vestartedretrieving{0}'sstatus".format(led.description))
brightness_level=led.get_brightness_level()
print("I'vefinishedretrieving{0}'sstatus".format(led.description))
response={
'id':led.identifier,
'description':led.description,
'brightness_level':brightness_level
}
self.set_status(status.HTTP_200_OK)
self.write(response)
defpatch(self,id):
int_id=int(id)
ifint_idnotindrone.leds.keys():
self.set_status(status.HTTP_404_NOT_FOUND)
return
led=drone.leds[int_id]
request_data=escape.json_decode(self.request.body)
if('brightness_level'notinrequest_data.keys())or\
(request_data['brightness_level']isNone):
self.set_status(status.HTTP_400_BAD_REQUEST)
return
try:
brightness_level=int(request_data['brightness_level'])
print("I'vestartedsettingthe{0}'sbrightness
level".format(led.description))
led.set_brightness_level(brightness_level)
print("I'vefinishedsettingthe{0}'sbrightness
level".format(led.description))
response={
'id':led.identifier,
'description':led.description,
'brightness_level':brightness_level
}
self.set_status(status.HTTP_200_OK)
self.write(response)
exceptValueErrorase:
print("I'vefailedsettingthe{0}'sbrightness
level".format(led.description))
self.set_status(status.HTTP_400_BAD_REQUEST)
response={
'error':e.args[0]
}
self.write(response)
TheLedHandlerclassisasubclassoftornado.web.RequestHandler.TheclassoverridestheSUPPORTED_METHODSclassvariablewithatuplethatindicatestheclassjustsupportstheGETandPATCHmethods.Inaddition,theclassdeclaresthefollowingtwomethodsthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestonthisHTTPhandler:
get:ThismethodreceivestheidoftheLEDwhosestatushastoberetrievedintheidargument.Ifthereceivedidisn'toneofthekeysofthedrone.ledsdictionary,thecodecallstheself.set_statusmethodwithstatus.HTTP_404_NOT_FOUNDasanargumenttosetthestatuscodefortheresponsetoHTTP404NotFound.Otherwise,thecoderetrievesthevalueassociatedwiththekeywhosevaluematchestheidinthedrone.ledsdictionaryandsavestheretrievedLightEmittingDiodeinstanceintheledvariable.ThecodeprintsamessageindicatingthatitstartedretrievingtheLED'sbrightnesslevel,callstheled.get_brightness_levelmethodwithasynchronousexecution,andsavestheresultinthebrightness_levelvariable.Then,thecodewritesamessageindicatingthatitfinishedretrievingthebrightnesslevelandgeneratesaresponsedictionarywiththe'id','description',and'brightness_level'keysandtheirvalues.Finally,thecodecallstheself.set_statusmethodwithstatus.HTTP_200_OKasanargumenttosetthestatuscodefortheresponsetoHTTP200OKandcallstheself.writemethodwiththeresponsedictionaryasanargument.Sinceresponseisadictionary,TornadoautomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Typeheadertoapplication/json.patch:ThismethodreceivestheidoftheLEDthathastobeupdatedorpatchedintheidargument.Ashappenedinthepreviouslyexplainedgetmethod,thecodereturnsanHTTP404NotFoundincasethereceivediddoesn'tmatchtheanyofthekeysofthedrone.ledsdictionary.Otherwise,thecodecallsthetornado.escape.json_decodemethodwithself.request.bodyasanargumenttogeneratePythonobjectsfortheJSONstringoftherequestbodyandsavesthegenerateddictionaryintherequest_datavariable.Ifthedictionarydoesn'tincludeakeynamed'brightness_level',thecodereturnsanHTTP400BadRequeststatuscode.Incasethereisakey,thecodeprintsa
messageindicatingthatitstartedsettingtheLED'sbrightnesslevel,includingthedescriptionfortheLED,callsthedrone.hexacopter.set_brightness_levelmethodwithasynchronousexecution.Ifthevaluespecifiedforthebrightness_levelisnotvalid,aValueErrorexceptionwillbecaughtandthecodewillreturnanHTTP400BadRequeststatuscodeandthevalidationerrormessagesastheresponsebody.Otherwise,thecodewritesamessageindicatingitfinishedsettingtheLED'sbrightnessvalueandgeneratesaresponsedictionarywiththe'id','description',and'brightness_level'keysandtheirvalues.Finally,thecodecallstheself.set_statusmethodwithstatus.HTTP_200_OKasanargumenttosetthestatuscodefortheresponsetoHTTP200OKandcallstheself.writemethodwiththeresponsedictionaryasanargument.Sinceresponseisadictionary,TornadoautomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Typeheadertoapplication/json.
Now,wewillcreateanAltimeterHandlerclassthatwewillusetorepresentthealtimeterresource.Openthepreviouslycreatedapi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
classAltimeterHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET")
ALTIMETER_ID=1
defget(self,id):
ifint(id)isnotself.__class__.ALTIMETER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
return
print("I'vestartedretrievingthealtitude")
altitude=drone.altimeter.get_altitude()
print("I'vefinishedretrievingthealtitude")
response={
'altitude':altitude
}
self.set_status(status.HTTP_200_OK)
self.write(response)
TheAltimeterHandlerclassisasubclassoftornado.web.RequestHandler.TheclassoverridestheSUPPORTED_METHODSclassvariablewithatuplethatindicatestheclassjustsupportstheGETmethod.Inaddition,theclassdeclaresthegetmethodthatwillbecalledwhentheHTTPmethodwiththesamenamearrivesasarequestonthisHTTPhandler.
Thegetmethodreceivestheidofthealtimeterwhosealtitudehastoberetrievedintheidargument.Ifthereceivediddoesn'tmatchthevalueoftheALTIMETER_IDclassattribute,thecodecallstheself.set_statusmethodwithstatus.HTTP_404_NOT_FOUNDasanargumenttosetthestatuscodefortheresponsetoHTTP404NotFound.Otherwise,thecodeprintsamessageindicatingthatitstartedretrievingthealtimeter'saltitude,callsthedrone.hexacopter.get_altitudemethodwithasynchronousexecution,andsavestheresultinthealtitudevariable.Then,thecodewritesamessageindicatingitfinishedretrievingthealtitudeandgeneratesaresponsedictionarywiththe'altitude'keyanditsvalue.Finally,thecodecallstheself.set_statusmethodwithstatus.HTTP_200_OKasanargumenttosetthe
statuscodefortheresponsetoHTTP200OKandcallstheself.writemethodwiththeresponsedictionaryasanargument.Sinceresponseisadictionary,TornadoautomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Typeheadertoapplication/json.
ThefollowingtableshowsthemethodofourpreviouslycreatedHTTPhandlerclassesthatwewanttobeexecutedforeachcombinationofHTTPverbandscope:
HTTPverb Scope Classandmethod
GET Hexacopter HexacopterHandler.get
PATCH Hexacopter HexacopterHandler.patch
GET LED LedHandler.get
PATCH LED LedHandler.patch
GET Altimeter AltimeterHandler.get
IftherequestresultsintheinvocationofanHTTPhandlerclasswithanunsupportedHTTPmethod,TornadowillreturnaresponsewiththeHTTP405MethodNotAllowedstatuscode.
MappingURLpatternstorequesthandlersWemustmapURLpatternstoourpreviouslycodedsubclassesoftornado.web.RequestHandler.Thefollowinglinescreatethemainentrypointfortheapplication,initializeitwiththeURLpatternsfortheAPI,andstartslisteningforrequests.Openthepreviouslycreatedapi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_09_01folder:
application=web.Application([
(r"/hexacopters/([0-9]+)",HexacopterHandler),
(r"/leds/([0-9]+)",LedHandler),
(r"/altimeters/([0-9]+)",AltimeterHandler),
],debug=True)
if__name__=="__main__":
port=8888
print("Listeningatport{0}".format(port))
application.listen(port)
ioloop.IOLoop.instance().start()
Theprecedingcodecreatesaninstanceoftornado.web.ApplicationnamedapplicationwiththecollectionofrequesthandlersthatmakeuptheWebapplication.ThecodepassesalistoftuplestotheApplicationconstructor.Thelistiscomposedofaregularexpression(regexp)andatornado.web.RequestHandlersubclass(request_class).Inaddition,thecodesetsthedebugargumenttoTruetoenabledebugging.
Themainmethodcallstheapplication.listenmethodtobuildanHTTPserverfortheapplicationwiththedefinedrulesonthespecifiedport.Inthiscase,thecodespecifies8888astheport,savedintheportvariable,whichisthedefaultportforTornadoHTTPservers.Then,thecalltotornado.ioloop.IOLoop.instance().start()startstheservercreatedwiththepreviouscalltotheapplication.listenmethod.
Tip
AswithanyotherWebframework,youshouldneverenabledebugginginaproductionenvironment.
MakingHTTPrequeststotheTornadoAPINow,wecanruntheapi.pyscriptthatlaunchesTornados'sdevelopmentservertocomposeandsendHTTPrequeststoourunsecureandsimpleWebAPI.Executethefollowingcommand:
pythonapi.py
Thefollowinglinesshowtheoutputafterweexecutethepreviouscommand.TheTornadoHTTPdevelopmentserverislisteningatport8888:
Listeningatport8888
Withthepreviouscommand,wewillstarttheTornadoHTTPserveranditwilllistenoneveryinterfaceonport8888.Thus,ifwewanttomakeHTTPrequeststoourAPIfromothercomputersordevicesconnectedtoourLAN,wedon'tneedanyadditionalconfigurations.
Tip
IfyoudecidetocomposeandsendHTTPrequestsfromothercomputersordevicesconnectedtotheLAN,rememberthatyouhavetousethedevelopmentcomputer'sassignedIPaddressinsteadoflocalhost.Forexample,ifthecomputer'sassignedIPv4IPaddressis192.168.1.103,insteadoflocalhost:8888,youshoulduse192.168.1.103:8888.Ofcourse,youcanalsousethehostnameinsteadoftheIPaddress.ThepreviouslyexplainedconfigurationsareveryimportantbecausemobiledevicesmightbetheconsumersofourRESTfulAPIsandwewillalwayswanttotesttheappsthatmakeuseofourAPIsinourdevelopmentenvironments.
TheTornadoHTTPserverisrunningonlocalhost(127.0.0.1),listeningonport8888,andwaitingforourHTTPrequests.Now,wewillcomposeandsendHTTPrequestslocallyinourdevelopmentcomputerorfromothercomputerordevicesconnectedtoourLAN.
Workingwithcommand-linetools–curlandhttpieWewillstartcomposingandsendingHTTPrequestswiththecommand-linetoolswehaveintroducedinChapter1,DevelopingRESTfulAPIswithDjango,curlandHTTPie.Incaseyouhaven'tinstalledHTTPie,makesureyouactivatethevirtualenvironmentandthenrunthefollowingcommandintheterminalorCommandPrompttoinstalltheHTTPiepackage:
pipinstall--upgradehttpie
Tip
Incaseyoudon'trememberhowtoactivatethevirtualenvironmentthatwecreatedforthisexample,readthefollowingsectioninthischapter—€”SettingupthevirtualenvironmentwithDjangoRESTFramework.
OpenaCygwinterminalinWindowsoraTerminalinmacOSorLinuxandrunthefollowingcommand.WewillcomposeandsendanHTTPrequesttoturnonthehexacopterandsetitsmotorspeedto100RPMs:
httpPATCH:8888/hexacopters/1motor_speed=100
Thefollowingistheequivalentcurlcommand.Itisveryimportanttousethe-H"Content-Type:application/json"optiontoindicatecurltosendthedataspecifiedafterthe-doptionasapplication/jsoninsteadofthedefaultapplication/x-www-form-urlencoded:
curl-iXPATCH-H"Content-Type:application/json"-d'{"motor_speed":100}'
:8888/hexacopters/1
TheprecedingcommandswillcomposeandsendthefollowingHTTPrequest,PATCHhttp://localhost:8888/hexacopters/1,withthefollowingJSONkey-valuepair:
{
"motor_speed":100
}
Therequestspecifies/hexacopters/1,andtherefore,Tornadowilliterateoverthelistoftupleswithregularexpressionsandrequestclassesanditwillmatch'/hexacopters/([0-9]+)'.TornadowillcreateaninstanceoftheHexacopterHandlerclassandruntheHexacopterHandler.patchmethodwith1asthevaluefortheidargument.AstheHTTPverbfortherequestisPATCH,Tornadocallsthepatchmethod.Ifthehexacopter'sspeedissuccessfullyset,themethodreturnsanHTTP200OKstatuscodeandthekey-valuepairswiththespeedandstatusfortherecentlyupdatedhexacopterserializedtoJSONintheresponsebody.ThefollowinglinesshowanexampleresponsefortheHTTPrequest:
HTTP/1.1200OK
Content-Length:33
Content-Type:application/json;charset=UTF-8
Date:Thu,08Sep201602:02:27GMT
Server:TornadoServer/4.4.1
{
"speed":100,
"turned_on":true
}
WewillcomposeandsendanHTTPrequesttoretrievethestatusandthemotorspeedforthehexacopter.GobacktotheCygwinterminalinWindowsortheTerminalinmacOSorLinux,andrunthefollowingcommand:
http:8888/hexacopters/1
Thefollowingistheequivalentcurlcommand:
curl-iXGET-H:8888/hexacopters/1
TheprecedingcommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8888/hexacopters/1.Therequestspecifies/hexacopters/1,andtherefore,itwillmatch'/hexacopters/([0-9]+)'andruntheHexacopterHandler.getmethodwith1asthevaluefortheidargument.AstheHTTPverbfortherequestisGET,Tornadocallsthegetmethod.Themethodretrievesthehexacopter'sstatusandgeneratesaJSONresponsewiththekey-valuepairs.
ThefollowinglinesshowanexampleresponsefortheHTTPrequest.ThefirstlinesshowtheHTTPresponseheaders,includingthestatus(200OK)andtheContent-typeas(application/json).AftertheHTTPresponseheaders,wecanseethedetailsofthehexacopter'sstatusintheJSONresponse:
HTTP/1.1200OK
Content-Length:33
Content-Type:application/json;charset=UTF-8
Date:Thu,08Sep201602:26:00GMT
Etag:"ff152383ca6ebe97e5a136166f433fbe7f9b4434"
Server:TornadoServer/4.4.1
{
"speed":100,
"turned_on":true
}
Afterwerunthethreerequests,wewillseethefollowinglinesinthewindowthatisrunningtheTornadoHTTPserver.Theoutputshowstheresultsofexecutingtheprintstatementsthatdescribewhenthecodestartedsettingorretrievinginformationandwhenitfinished:
I'vestartedsettingthehexacopter'smotorspeed
I'vefinishedsettingthehexacopter'smotorspeed
I'vestartedretrievinghexacopter'sstatus
I'vefinishedretrievinghexacopter'sstatus
Thedifferentmethodswecodedintherequesthandlerclassesendupcallingtime.sleeptosimulateittakessometimefortheoperationswiththehexacopter.Inthiscase,ourcodeisrunningwithasynchronousexecution,andtherefore,eachtimewecomposeandsenda
request,theTornadoserverisblockeduntiltheoperationwiththehexacopterfinishesandthemethodsendstheresponse.WewillcreateanewversionofthisAPIthatwilluseasynchronousexecutionlaterandwewillunderstandtheadvantagesofTornado'snon-blockingfeatures.However,first,wewillunderstandhowthesynchronousversionoftheAPIworks.
ThefollowingimageshowstwoTerminalwindowsside-by-sideonmacOS.TheTerminalwindowontheleft-handsideisrunningtheTornadoHTTPserveranddisplaysthemessagesprintedinthemethodsthatprocesstheHTTPrequests.TheTerminalwindowontheright-handsideisrunninghttpcommandstogeneratetheHTTPrequests.ItisagoodideatouseasimilarconfigurationtochecktheoutputwhilewecomposeandsendtheHTTPrequests:
Now,wewillcomposeandsendanHTTPrequesttoretrieveahexacopterthatdoesn'texist.Rememberthatwejusthaveonehexacopterinourdrone.Runthefollowingcommandtotrytoretrievethestatusforanhexacopterwithaninvalidid.Wemustmakesurethattheutilitiesdisplaytheheadersaspartoftheresponsetoseethereturnedstatuscode:
http:8888/hexacopters/8
Thefollowingistheequivalentcurlcommand:
curl-iXGET:8888/hexacopters/8
ThepreviouscommandswillcomposeandsendthefollowingHTTPrequest:GEThttp://localhost:8888/hexacopters/8.Therequestisthesameasthepreviousonewehaveanalyzed,withadifferentnumberfortheidparameter.TheserverwillruntheHexacopterHandler.getmethodwith8asthevaluefortheidargument.Theidisnotequalto1,andtherefore,thecodewillreturnanHTTP404NotFoundstatuscode.Thefollowing
linesshowanexampleheaderresponsefortheHTTPrequest:
HTTP/1.1404NotFound
Content-Length:0
Content-Type:text/html;charset=UTF-8
Date:Thu,08Sep201604:31:53GMT
Server:TornadoServer/4.4.1
WorkingwithGUItools-PostmanandothersSofar,wehavebeenworkingwithtwoTerminal-basedorcommand-linetoolstocomposeandsendHTTPrequeststoourTornadoHTTPserver-cURLandHTTPie.Now,wewillworkwithoneoftheGUItoolsweusedwhencomposingandsendingHTTPrequeststotheDjangodevelopmentserverandtheFlaskdevelopmentserver:Postman.
Now,wewillusetheBuildertabinPostmantoeasilycomposeandsendHTTPrequeststolocalhost:8888andtesttheRESTfulAPIwiththisGUItool.RememberthatPostmandoesn'tsupportcurl-likeshorthandsforlocalhost,andtherefore,wecannotusethesameshorthandswehavebeenusingwhencomposingrequestswithcurlandHTTPie.
SelectGET inthedrop-downmenuattheleft-handsideoftheEnterrequestURLtextboxandenterlocalhost:8888/leds/1inthistextboxattheright-handsideofthedropdown.Now,clickonSendandPostmanwilldisplaythestatus(200OK),thetimeittookfortherequesttobeprocessedandtheresponsebodywithallthegamesformattedasJSONwithsyntaxhighlighting(Prettyview).
ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPGETrequest:
ClickonHeadersontheright-handsideofBodyandCookiestoreadtheresponseheaders.ThefollowingscreenshotshowsthelayoutfortheresponseheadersthatPostmandisplaysforthepreviousresponse.NotethatPostmandisplaystheStatusattheright-handsideoftheresponseanddoesn'tincludeitasthefirstlineoftheHeaders,asithappenedwhenweworked
withboththecURLandHTTPieutilities:
Now,wewillusetheBuildertabinPostmantocomposeandsendanHTTPrequesttocreateanewmessage,specifically,aPATCHrequest.Followthenextsteps:
1. SelectPATCHfromthedrop-downmenuontheleft-handsideoftheEnterrequestURLtextboxandenterlocalhost:8888/leds/1inthistextboxattheright-handsideofthedropdown.
2. ClickonBodyontheright-handsideofAuthorizationandHeaders,withinthepanelthatcomposestherequest.
3. ActivatetherawradiobuttonandselectJSON(application/json)inthedropdownontheright-handsideofthebinaryradiobutton.PostmanwillautomaticallyaddaContent-type=application/jsonheader,andtherefore,youwillnoticetheHeaderstabwillberenamedtoHeaders(1),indicatingusthatthereisonekey-valuepairspecifiedfortherequestheaders.
4. Enterthefollowinglinesinthetextboxbelowtheradiobuttons,withintheBodytab:
{
"brightness_level":128
}
ThefollowingscreenshotshowstherequestbodyinPostman:
WefollowedthenecessarystepstocreateanHTTPPATCHrequestwithaJSONbodythatspecifiesthenecessarykey-valuepairstocreateanewgame.ClickonSendandPostmanwilldisplaytheStatus(200OK),thetimeittookfortherequesttobeprocessed,andtheresponsebodywiththerecentlyaddedgameformattedasJSONwithsyntaxhighlighting(Prettyview).ThefollowingscreenshotshowstheJSONresponsebodyinPostmanfortheHTTPPOSTrequest.
TheTornadoHTTPserverislisteningoneveryinterfaceonport8888,andtherefore,wecanalsouseappsthatcancomposeandsendHTTPrequestsfrommobiledevicestoworkwiththeRESTfulAPI.Forexample,wecanworkwiththepreviouslyintroducediCurlHTTPapponiOSdevicessuchasiPadProandiPhone.InAndroiddevices,wecanworkwiththepreviouslyintroducedHTTPRequestApp.
ThefollowingscreenshotshowstheresultsofcomposingandsendingthefollowingHTTPrequestwiththeiCurlHTTPapp—GEThttp://192.168.2.3:8888/altimeters/1.RememberthatyouhavetoperformthepreviouslyexplainedconfigurationsinyourLANandrouterto
beabletoaccesstheFlaskdevelopmentserverfromotherdevicesconnectedtoyourLAN.Inthiscase,theIPassignedtothecomputerrunningtheTornadoHTTPserveris192.168.2.3,andtherefore,youmustreplacethisIPwiththeIPassignedtoyourdevelopmentcomputer:
Testyourknowledge1. ThemainbuildingblocksforaRESTfulAPIinTornadoaresubclassesofwhichthe
followingclasses:1. tornado.web.GenericHandler2. tornado.web.RequestHandler3. tornado.web.IncomingHTTPRequestHandler
2. IfwejustwanttosupporttheGETandPATCHmethods,wecanoverridetheSUPPORTED_METHODSclassvariablewithwhichofthefollowingvalues:1. ("GET","PATCH")2. {0:"GET",1:"PATCH"}3. {"GET":True,"PATCH":True,"POST":False,"PUT":False}
3. Thelistoftuplesforathetornado.Web.Applicationconstructoriscomposedof:1. Aregularexpression(regexp)andatornado.web.RequestHandlersubclass
(request_class).2. Aregularexpression(regexp)andatornado.web.GenericHandlersubclass
(request_class).3. Aregularexpression(regexp)andatornado.web.IncomingHTTPRequestHandler
subclass(request_class).
4. Whenwecalltheself.writemethodwithadictionaryasanargumentinarequesthandler,Tornado:1. AutomaticallywritesthechunkasJSONbutwehavetomanuallysetthevalueofthe
Content-Typeheadertoapplication/json.2. Requiresustousethejson.dumpsmethodandsetthevalueoftheContent-Type
headertoapplication/json.3. AutomaticallywritesthechunkasJSONandsetsthevalueoftheContent-Type
headertoapplication/json.
5. Acallstothetornado.escape.json_decodemethodwithself.request.bodyasanargumentinarequesthandler:1. GeneratesPythonobjectsfortheJSONstringoftherequestbodyandreturnsthe
generatedtuple.2. GeneratesPythonobjectsfortheJSONstringoftherequestbodyandreturnsthe
generateddictionary.3. GeneratesPythonobjectsfortheJSONstringoftherequestbodyandreturnsthe
generatedlist.
SummaryInthischapter,wedesignedaRESTfulAPItointeractwithslowsensorsandactuators.WedefinedtherequirementsforourAPI,understoodthetasksperformedbyeachHTTPmethod,andsetupavirtualenvironmentwithTornado.
WecreatedtheclassesthatrepresentadroneandwrotecodetosimulateslowI/OoperationsthatarecalledforeachHTTPrequestmethod,wroteclassesthatrepresentrequesthandlersandprocessthedifferentHTTPrequests,andconfiguredtheURLpatternstorouteURLstorequesthandlersandtheirmethods.
Finally,westartedTornadodevelopmentserver,usedcommand-linetoolstocomposeandsendHTTPrequeststoourRESTfulAPI,andanalyzedhoweachHTTPrequestswasprocessedinourcode.WealsoworkedwithGUItoolstocomposeandsendHTTPrequests.
NowthatweunderstandthebasicsofTornadotocreateRESTfulAPIs,wewilltakeadvantageofthenon-blockingfeaturescombinedwithasynchronousoperationsinTornadoinanewversionfortheAPI,whichiswhatwearegoingtodiscussinthenextchapter.
Chapter10.WorkingwithAsynchronousCode,Testing,andDeployinganAPIwithTornadoInthischapter,wewilltakeadvantageofthenon-blockingfeaturescombinedwithasynchronousoperationsinTornadoinanewversionfortheAPIwebuiltinthepreviouschapter.Wewillconfigure,write,andexecuteunittestsandlearnafewthingsrelatedtodeployment.Wewillcoverthefollowingtopics:
UnderstandingsynchronousandasynchronousexecutionWorkingwithasynchronouscodeRefactoringcodetotakeadvantageofasynchronousdecoratorsMappingURLpatternstoasynchronousandnon-blockingrequesthandlersMakingHTTPrequeststotheTornadonon-blockingAPISettingupunittestsWritingafirstroundofunittestsRunningunittestswithnose2andcheckingtestingcoverageImprovingtestingcoverage
UnderstandingsynchronousandasynchronousexecutionInourcurrentversionoftheAPI,eachHTTPrequestisblocking,ashappenedwithDjangoandFlask.Thus,whenevertheTornadoHTTPserverreceivesanHTTPrequest,itdoesn'tstartworkingonanyotherHTTPrequestintheincomingqueueuntiltheserversendstheresponseforthefirstHTTPrequestitreceived.Themethodswecodedintherequesthandlersareworkingwithasynchronousexecutionandtheydon'ttakeadvantageofthenon-blockingfeaturesincludedinTornadowhencombinedwithasynchronousexecutions.
InordertosetthebrightnesslevelforboththeblueandwhiteLEDs,wehavetomaketwoHTTPPATCHrequests.WewillmakethemtounderstandhowourcurrentversionoftheAPIprocessestwoincomingrequests.
OpentwoCygwinterminalsinWindowsortwoTerminalsinmacOSorLinux,andwritethefollowingcommandinthefirstone.WewillcomposeandsendanHTTPrequesttosetthebrightnesslevelfortheblueLEDto255.Writethelineinthefirstwindow,butdon'tpressEnteryet,aswewilltrytolaunchtwocommandsatalmostthesametimeintwowindows:
httpPATCH:8888/leds/1brightness_level=255
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d
'{"brightness_level":255}':8888/leds/1
Now,gotothesecondwindowandwritethefollowingcommand.WewillcomposeandsendanHTTPrequesttosetthebrightnesslevelforthewhiteLEDto255.Writethelineinthesecondwindow,butdon'tpressEnteryet,aswewilltrytolaunchtwocommandsatalmostthesametimeintwowindows:
httpPATCH:8888/leds/2brightness_level=255
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d
'{"brightness_level":255}':8888/leds/2
Now,gotothefirstwindow,pressEnter.Then,gotothesecondwindowandquicklypressEnter.YouwillseethefollowinglineinthewindowthatisrunningtheTornadoHTTPserver:
I'vestartedsettingtheBlueLED'sbrightnesslevel
Then,youwillseethefollowinglinesthatshowtheresultsofexecutingtheprintstatementsthatdescribewhenthecodefinishedandthenstartedsettingthebrightnesslevelfortheLEDs:
I'vefinishedsettingtheBlueLED'sbrightnesslevel
I'vestartedsettingtheWhiteLED'sbrightnesslevel
I'vefinishedsettingtheWhiteLED'sbrightnesslevel
ItwasnecessarytowaitfortherequestthatchangedthebrightnesslevelfortheblueLEDtofinishbeforetheservercouldprocesstheHTTPthatchangesthebrightnesslevelforthewhiteLED.ThefollowingscreenshotshowsthreewindowsonWindows.Thewindowontheleft-handsideisrunningtheTornadoHTTPserveranddisplaysthemessagesprintedinthemethodsthatprocesstheHTTPrequests.Thewindowattheupper-rightcornerisrunningthehttpcommandtogeneratetheHTTPrequestthatchangesthebrightnesslevelfortheblueLED.Thewindowatthelower-rightcornerisrunningthehttpcommandtogeneratetheHTTPrequestthatchangesthebrightnesslevelforthewhiteLED.ItisagoodideatouseasimilarconfigurationtochecktheoutputwhilewecomposeandsendtheHTTPrequestsandhowthesynchronousexecutionisworkingonthecurrentversionoftheAPI:
Tip
Rememberthatthedifferentmethodswecodedintherequesthandlerclassesendupcallingtime.sleeptosimulateittakessometimefortheoperationstocompletetheirexecution.
AseachoperationtakessometimeandblocksthepossibilitytoprocessotherincomingHTTPrequests,wewillcreateanewversionofthisAPIthatwilluseasynchronousexecution,andwewillunderstandtheadvantagesofTornado'snon-blockingfeatures.Thisway,itwillbepossibletochangethebrightnesslevelforthewhiteLEDwhiletheotherrequestisto
changethebrightnesslevelfortheblueLED.TornadowillbeabletostartprocessingrequestswhiletheI/Ooperationswiththedronetakesometimetocomplete.
RefactoringcodetotakeadvantageofasynchronousdecoratorsItisextremelydifficulttoreadandunderstandcodesplitintodifferentmethods,suchastheasynchronouscodethatrequiresworkingwithcallbacksthatareexecutedoncetheasynchronousexecutionfinishes.Luckily,Tornadoprovidesagenerator-basedinterfacethatenablesustowriteasynchronouscodeinrequesthandlersinasinglegenerator.Wecanavoidsplittingourmethodsintomultiplemethodswithcallbacksbyusingthetornado.gengenerator-basedinterfacethatTornadoprovidestomakeiteasiertoworkinanasynchronousenvironment.
TherecommendedwaytowriteasynchronouscodeinTornadoistousecoroutines.Thus,wewillrefactorourexistingcodetousethe@tornado.gen.coroutinedecoratorforasynchronousgeneratorsintherequiredmethodsthatprocessthedifferentHTTPrequestsinthesubclassesoftornado.web.RequestHandler.
Tip
Insteadofworkingwithachainofcallbacks,coroutinesusethePythonyieldkeywordtosuspendandresumeexecution.Byusingcoroutines,ourcodeisgoingtobeassimpletounderstandandmaintainasifwewerewritingsynchronouscode.
Wewilluseaninstanceoftheconcurrent.futures.ThreadPoolExecutorclassthatprovidesuswithahigh-levelinterfaceforasynchronouslyexecutingcallables.Theasynchronousexecutionwillbeperformedwiththreads.Wewillalsousethe@tornado.concurrent.run_on_executordecoratortorunasynchronousmethodasynchronouslyonanexecutor.Inthiscase,themethodsprovidedbythedifferentcomponentsofourdronetogetandsetdatahaveasynchronousexecution.Wewantthemtorunwithanasynchronousexecution.
Createanewasync_api.pyfile.Thefollowinglinesshowallthenecessaryimportsfortheclassesthatwewillcreateandthecodethatcreatesaninstanceoftheconcurrent.futures.ThreadPoolExecutorclassnamedthread_pool.Wewillusethisinstanceinthedifferentmethodsthatwewillrefactortomakeasynchronouscalls.Thecodefileforthesampleisincludedintherestful_python_chapter_10_01folder:
importstatus
fromdatetimeimportdate
fromtornadoimportweb,escape,ioloop,httpclient,gen
fromconcurrent.futuresimportThreadPoolExecutor
fromtornado.concurrentimportrun_on_executor
fromdroneimportAltimeter,Drone,Hexacopter,LightEmittingDiode
thread_pool=ThreadPoolExecutor()
drone=Drone()
Now,wewillcreateanAsyncHexacopterHandlerclassthatwewillusetohandlerequestsforthehexacopterresourcewithanasynchronousexecution.ThelinesthatareneworchangedcomparedwiththesynchronousversionofthishandlernamedHexacopterHandlerarehighlighted.Openthepreviouslycreatedasync_pi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_10_01folder:
classAsyncHexacopterHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET","PATCH")
HEXACOPTER_ID=1
_thread_pool=thread_pool
@gen.coroutine
defget(self,id):
ifint(id)isnotself.__class__.HEXACOPTER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
self.finish()
return
print("I'vestartedretrievinghexacopter'sstatus")
hexacopter_status=yieldself.retrieve_hexacopter_status()
print("I'vefinishedretrievinghexacopter'sstatus")
response={
'speed':hexacopter_status.motor_speed,
'turned_on':hexacopter_status.turned_on,
}
self.set_status(status.HTTP_200_OK)
self.write(response)
self.finish()
@run_on_executor(executor="_thread_pool")
defretrieve_hexacopter_status(self):
returndrone.hexacopter.get_hexacopter_status()
@gen.coroutine
defpatch(self,id):
ifint(id)isnotself.__class__.HEXACOPTER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
self.finish()
return
request_data=escape.json_decode(self.request.body)
if('motor_speed'notinrequest_data.keys())or\
(request_data['motor_speed']isNone):
self.set_status(status.HTTP_400_BAD_REQUEST)
self.finish()
return
try:
motor_speed=int(request_data['motor_speed'])
print("I'vestartedsettingthehexacopter'smotorspeed")
hexacopter_status=yield
self.set_hexacopter_motor_speed(motor_speed)
print("I'vefinishedsettingthehexacopter'smotorspeed")
response={
'speed':hexacopter_status.motor_speed,
'turned_on':hexacopter_status.turned_on,
}
self.set_status(status.HTTP_200_OK)
self.write(response)
self.finish()
exceptValueErrorase:
print("I'vefailedsettingthehexacopter'smotorspeed")
self.set_status(status.HTTP_400_BAD_REQUEST)
response={
'error':e.args[0]
}
self.write(response)
self.finish()
@run_on_executor(executor="_thread_pool")
defset_hexacopter_motor_speed(self,motor_speed):
returndrone.hexacopter.set_motor_speed(motor_speed)
TheAsyncHexacopterHandlerclassdeclaresa_thread_poolclassattributethatsavesareferencetothepreviouslycreatedconcurrent.futures.ThreadPoolExecutorinstance.Theclassdeclarestwomethodswiththe@run_on_executor(executor="_thread_pool")decoratorthatmakesthesynchronousmethodrunasynchronouslywiththeconcurrent.futures.ThreadPoolExecutorinstancewhosereferenceissavedinthe_thread_poolclassattribute.Thefollowingarethetwomethods:
retrieve_hexacopter_status:Thismethodreturnstheresultsofcallingthedrone.hexacopter.get_hexacopter_statusmethod.set_hexacopter_motor_speed:Thismethodreceivesthemotor_speedargumentandreturnstheresultsofcallingthedrone.hexacopter.set_motor_speedmethodwiththereceivedmotor_speedasanargument.
Weaddedthe@gen.coroutinedecoratortoboththegetandpatchmethods.Weaddedacalltoself.finishwheneverwewantedtofinishtheHTTPrequest.ItisourresponsibilitytocallthismethodtofinishtheresponseandendtheHTTPrequestwhenweusethe@gen.coroutinedecorator.
Thegetmethodusesthefollowinglinetoretrievethehexacopterstatuswithanon-blockingandasynchronousexecution:
hexacopter_status=yieldself.retrieve_hexacopter_status()
ThecodeusestheyieldkeywordtoretrieveHexacopterStatusfromtheFuturereturnedbyself.retrieve_hexacopter_statusthatrunswithanasynchronousexecution.AFutureencapsulatestheasynchronousexecutionofacallable.Inthiscase,Futureencapsulatestheasynchronousexecutionoftheself.retrieve_hexacopter_statusmethod.Thenextlinesdidn'trequirechanges,andweonlyhadtoaddacalltoself.finishasthelastlineafterwewritetheresponse.
Thegetmethodusesthefollowinglinetoretrievethehexacopterstatuswithanon-blockingandasynchronousexecution:
hexacopter_status=yieldself.retrieve_hexacopter_status()
ThecodeusestheyieldkeywordtoretrievetheHexacopterStatusfromtheFuturereturnedbytheself.retrieve_hexacopter_statusthatrunswithanasynchronousexecution.
Thepatchmethodusesthefollowinglinetosetthehexacopter'smotorspeedwithanon-blockingandasynchronousexecution:
hexacopter_status=yieldself.set_hexacopter_motor_speed(motor_speed)
ThecodeusestheyieldkeywordtoretrievetheHexacopterStatusfromtheFuturereturnedbytheself.set_hexacopter_motor_speedthatrunswithanasynchronousexecution.Thenextlinesdidn'trequirechanges,andweonlyhadtoaddacalltoself.finishasthelastlineafterwewritetheresponse.
Now,wewillcreateanAsyncLedHandlerclassthatwewillusetorepresenttheLEDresourcesandprocessrequestswithanasynchronousexecution.ThelinesthatareneworchangedcomparedwiththesynchronousversionofthishandlernamedLedHandlerarehighlighted.Openthepreviouslycreatedasync_pi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_10_01folder:
classAsyncLedHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET","PATCH")
_thread_pool=thread_pool
@gen.coroutine
defget(self,id):
int_id=int(id)
ifint_idnotindrone.leds.keys():
self.set_status(status.HTTP_404_NOT_FOUND)
self.finish()
return
led=drone.leds[int_id]
print("I'vestartedretrieving{0}'sstatus".format(led.description))
brightness_level=yield
self.retrieve_led_brightness_level(led)
print("I'vefinishedretrieving{0}'sstatus".format(led.description))
response={
'id':led.identifier,
'description':led.description,
'brightness_level':brightness_level
}
self.set_status(status.HTTP_200_OK)
self.write(response)
self.finish()
@run_on_executor(executor="_thread_pool")
defretrieve_led_brightness_level(self,led):
returnled.get_brightness_level()
@gen.coroutine
defpatch(self,id):
int_id=int(id)
ifint_idnotindrone.leds.keys():
self.set_status(status.HTTP_404_NOT_FOUND)
self.finish()
return
led=drone.leds[int_id]
request_data=escape.json_decode(self.request.body)
if('brightness_level'notinrequest_data.keys())or\
(request_data['brightness_level']isNone):
self.set_status(status.HTTP_400_BAD_REQUEST)
self.finish()
return
try:
brightness_level=int(request_data['brightness_level'])
print("I'vestartedsettingthe{0}'sbrightness
level".format(led.description))
yieldself.set_led_brightness_level(led,brightness_level)
print("I'vefinishedsettingthe{0}'sbrightness
level".format(led.description))
response={
'id':led.identifier,
'description':led.description,
'brightness_level':brightness_level
}
self.set_status(status.HTTP_200_OK)
self.write(response)
self.finish()
exceptValueErrorase:
print("I'vefailedsettingthe{0}'sbrightness
level".format(led.description))
self.set_status(status.HTTP_400_BAD_REQUEST)
response={
'error':e.args[0]
}
self.write(response)
self.finish()
@run_on_executor(executor="_thread_pool")
defset_led_brightness_level(self,led,brightness_level):
returnled.set_brightness_level(brightness_level)
TheAsyncLedHandlerclassdeclaresa_thread_poolclassattributethatsavesareferencetothepreviouslycreatedconcurrent.futures.ThreadPoolExecutorinstance.Theclassdeclarestwomethodswiththe@run_on_executor(executor="_thread_pool")decoratorthatmakesthesynchronousmethodrunasynchronouslywiththeconcurrent.futures.ThreadPoolExecutorinstancewhosereferenceissavedinthe_thread_poolclassattribute.Thefollowingarethetwomethods:
retrieve_led_brightness_level:ThismethodreceivesaLightEmittingDiodeinstanceintheledargumentandreturnstheresultsofcallingtheled.get_brightness_levelmethod.set_led_brightness_level:ThismethodreceivesaLightEmittingDiodeinstanceintheledargumentandthebrightness_levelargument.Thecodereturnstheresultsofcallingtheled.set_brightness_levelmethodwiththereceivedbrightness_levelasanargument.
Weaddedthe@gen.coroutinedecoratortoboththegetandpatchmethods.Inaddition,weaddedacalltoself.finishwheneverwewantedtofinishtheHTTPrequest.
ThegetmethodusesthefollowinglinetoretrievetheLED'sbrightnesslevelwithanon-blockingandasynchronousexecution:
brightness_level=yieldself.retrieve_led_brightness_level(led)
ThecodeusestheyieldkeywordtoretrievetheintfromFuturereturnedbyself.retrieve_led_brightness_levelthatrunswithanasynchronousexecution.Thenextlinesdidn'trequirechanges,andweonlyhadtoaddacalltoself.finishasthelastlineafterwewritetheresponse.
Thepatchmethodusesthefollowinglinetoretrievethehexacopterstatuswithanon-blockingandasynchronousexecution:
hexacopter_status=yieldself.retrieve_hexacopter_status()
ThecodeusestheyieldkeywordtoretrieveHexacopterStatusfromFuturereturnedbyself.retrieve_hexacopter_statusthatrunswithanasynchronousexecution.
ThepatchmethodusesthefollowinglinetosettheLED'sbrightnesslevelwithanon-blockingandasynchronousexecution:
yieldself.set_led_brightness_level(led,brightness_level)
Thecodeusestheyieldkeywordtocallself.set_led_brightness_levelwithanasynchronousexecution.Thenextlinesdidn'trequirechanges,andweonlyhadtoaddacalltoself.finishasthelastlineafterwewritetheresponse.
Now,wewillcreateanAsyncAltimeterHandlerclassthatwewillusetorepresentthealtimeterresourceandprocessthegetrequestwithanasynchronousexecution.ThelinesthatareneworchangedcomparedwiththesynchronousversionofthishandlernamedAltimeterHandler,arehighlighted.Openthepreviouslycreatedasync_pi.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_10_01folder.
classAsyncAltimeterHandler(web.RequestHandler):
SUPPORTED_METHODS=("GET")
ALTIMETER_ID=1
_thread_pool=thread_pool
@gen.coroutine
defget(self,id):
ifint(id)isnotself.__class__.ALTIMETER_ID:
self.set_status(status.HTTP_404_NOT_FOUND)
self.finish()
return
print("I'vestartedretrievingthealtitude")
altitude=yieldself.retrieve_altitude()
print("I'vefinishedretrievingthealtitude")
response={
'altitude':altitude
}
self.set_status(status.HTTP_200_OK)
self.write(response)
self.finish()
@run_on_executor(executor="_thread_pool")
defretrieve_altitude(self):
returndrone.altimeter.get_altitude()
TheAsyncAltimeterHandlerclassdeclaresa_thread_poolclassattributethatsavesareferencetothepreviouslycreatedconcurrent.futures.ThreadPoolExecutorinstance.Theclassdeclarestheretrieve_altitudemethodwiththe@run_on_executor(executor="_thread_pool")decoratorthatmakesthesynchronousmethodrunasynchronouslywiththeconcurrent.futures.ThreadPoolExecutorinstancewhosereferenceissavedinthe_thread_poolclassattribute.Theretrieve_altitudemethodreturnstheresultsofcallingthedrone.altimeter.get_altitudemethod.
[email protected],weaddedacalltoself.finishwheneverwewantedtofinishtheHTTPrequest.
Thegetmethodusesthefollowinglinetoretrievethealtimeter'saltitudevaluewithanon-blockingandasynchronousexecution:
altitude=yieldself.retrieve_altitude()
ThecodeusestheyieldkeywordtoretrievetheintfromFuturereturnedbyself.retrieve_altitudethatrunswithanasynchronousexecution.Thenextlinesdidn'trequirechanges,andweonlyhadtoaddacalltoself.finishasthelastlineafterwewritetheresponse.
MappingURLpatternstoasynchronousrequesthandlersWemustmapURLpatternstoourpreviouslycodedsubclassesoftornado.web.RequestHandlerthatprovideusasynchronousmethodsforourrequesthandlers.Thefollowinglinescreatethemainentrypointfortheapplication,initializeitwiththeURLpatternsfortheAPI,andstartlisteningforrequests.Openthepreviouslycreatedasync_api.pyfileandaddthefollowinglines.Thecodefileforthesampleisincludedintherestful_python_chapter_10_01folder:
application=web.Application([
(r"/hexacopters/([0-9]+)",AsyncHexacopterHandler),
(r"/leds/([0-9]+)",AsyncLedHandler),
(r"/altimeters/([0-9]+)",AsyncAltimeterHandler),
],debug=True)
if__name__=="__main__":
port=8888
print("Listeningatport{0}".format(port))
application.listen(port)
ioloop.IOLoop.instance().start()
Thecodecreatesaninstanceoftornado.web.ApplicationnamedapplicationwiththecollectionofrequesthandlersthatmakeuptheWebapplication.WejustchangedthenameofthehandlerswiththenewnamesthathavetheAsyncprefix.
Tip
AswithanyotherWebframework,youshouldneverenabledebugginginaproductionenvironment.
MakingHTTPrequeststotheTornadonon-blockingAPINow,wecanruntheasync_api.pyscriptthatlaunchesTornados'sdevelopmentservertocomposeandsendHTTPrequeststoournewversionoftheWebAPIthatusesthenon-blockingfeaturesofTornadocombinedwithasynchronousexecution.Executethefollowingcommand:
pythonasync_api.py
Thefollowinglinesshowtheoutputafterweexecutethepreviouscommand.TheTornadoHTTPdevelopmentserverislisteningatport8888:
Listeningatport8888
Withthepreviouscommand,wewillstarttheTornadoHTTPserveranditwilllistenoneveryinterfaceonport8888.Thus,ifwewanttomakeHTTPrequeststoourAPIfromothercomputersordevicesconnectedtoourLAN,wedon'tneedanyadditionalconfigurations.
InournewversionoftheAPI,eachHTTPrequestisnon-blocking.Thus,whenevertheTornadoHTTPserverreceivesanHTTPrequestandmakesanasynchronouscall,itisabletostartworkingonanyotherHTTPrequestintheincomingqueuebeforetheserversendstheresponseforthefirstHTTPrequestitreceived.Themethodswecodedintherequesthandlersareworkingwithanasynchronousexecutionandtheytakeadvantageofthenon-blockingfeaturesincludedinTornado,combinedwithasynchronousexecutions.
InordertosetthebrightnesslevelforboththeblueandwhiteLEDs,wehavetomaketwoHTTPPATCHrequests.WewillmakethemtounderstandhowournewversionoftheAPIprocessestwoincomingrequests.
OpentwoCygwinterminalsinWindows,ortwoTerminalsinmacOSorLinux,andwritethefollowingcommandinthefirstone.WewillcomposeandsendanHTTPrequesttosetthebrightnesslevelfortheblueLEDto255.Writethelineinthefirstwindowbutdon'tpressEnteryet,aswewilltrytolaunchtwocommandsatalmostthesametimeintwowindows:
httpPATCH:8888/leds/1brightness_level=255
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d
'{"brightness_level":255}':8888/leds/1
Now,gotothesecondwindowandwritethefollowingcommand.WewillcomposeandsendanHTTPrequesttosetthebrightnesslevelforthewhiteLEDto255.Writethelineinthesecondwindowbutdon'tpressEnteryet,aswewilltrytolaunchtwocommandsatalmostthesametimeintwowindows:
httpPATCH:8888/leds/2brightness_level=255
Thefollowingistheequivalentcurlcommand:
curl-iXPATCH-H"Content-Type:application/json"-d
'{"brightness_level":255}':8888/leds/2
Now,gotothefirstwindow,pressEnter.Then,gotothesecondwindowandquicklypressEnter.YouwillseethefollowinglinesinthewindowthatisrunningtheTornadoHTTPserver:
I'vestartedsettingtheBlueLED'sbrightnesslevel
I'vestartedsettingtheWhiteLED'sbrightnesslevel
Then,youwillseethefollowinglinesthatshowtheresultsofexecutingtheprintstatementsthatdescribewhenthecodefinishedsettingthebrightnesslevelfortheLEDs:
I'vefinishedsettingtheBlueLED'sbrightnesslevel
I'vefinishedsettingtheWhiteLED'sbrightnesslevel
TheservercouldstartprocessingtherequestthatchangesthebrightnesslevelforthewhiteLEDbeforetherequestthatchangesthebrightnessleveloftheblueLEDfinishesitsexecution.ThefollowingscreenshotshowsthreewindowsonWindows.Thewindowontheleft-handsideisrunningtheTornadoHTTPserveranddisplaysthemessagesprintedinthemethodsthatprocesstheHTTPrequests.Thewindowontheupper-rightcornerisrunningthehttpcommandtogeneratetheHTTPrequestthatchangesthebrightnesslevelfortheblueLED.Thewindowatthelower-rightcornerisrunningthehttpcommandtogeneratetheHTTPrequestthatchangesthebrightnesslevelforthewhiteLED.ItisagoodideatouseasimilarconfigurationtochecktheoutputwhilewecomposeandsendtheHTTPrequestsandcheckhowtheasynchronousexecutionisworkingonthenewversionoftheAPI:
Eachoperationtakessometimebutdoesn'tblockthepossibilitytoprocessotherincomingHTTPrequeststhankstothechangeswemadetotheAPItotakeadvantageoftheasynchronousexecution.Thisway,itispossibletochangethebrightnesslevelforthewhiteLEDwhiletheotherrequestistochangethebrightnesslevelfortheblueLED.TornadoisabletostartprocessingrequestswhiletheI/Ooperationswiththedronetakesometimetocomplete.
SettingupunittestsWewillusenose2tomakeiteasiertodiscoverandrununittests.Wewillmeasuretestcoverage,andtherefore,wewillinstallthenecessarypackagetoallowustoruncoveragewithnose2.First,wewillinstallthenose2andcov-corepackagesinourvirtualenvironment.Thecov-corepackagewillallowustomeasuretestcoveragewithnose2.
MakesureyouquittheTornado'sHTTPserver.RememberthatyoujustneedtopressCtrl+CintheTerminalorcommand-promptwindowinwhichitisrunning.Wejustneedtorunthefollowingcommandtoinstallthenose2packagethatwillalsoinstallthesixdependency:
pipinstallnose2
Thelastlinesfortheoutputwillindicatethatthenose2packagehasbeensuccessfullyinstalled:
Collectingnose2
Collectingsix>=1.1(fromnose2)
Downloadingsix-1.10.0-py2.py3-none-any.whl
Installingcollectedpackages:six,nose2
Successfullyinstallednose2-0.6.5six-1.10.0
Wejustneedtorunthefollowingcommandtoinstallthecov-corepackagethatwillalsoinstallthecoveragedependency:
pipinstallcov-core
Thelastlinesfortheoutputwillindicatethatthedjango-nosepackagehasbeensuccessfullyinstalled:
Collectingcov-core
Collectingcoverage>=3.6(fromcov-core)
Installingcollectedpackages:coverage,cov-core
Successfullyinstalledcov-core-1.15.0coverage-4.2
Openthepreviouslycreatedasync_api.pyfileandremovethelinesthatcreatetheweb.Applicationinstancenamedapplicationandthe__main__method.Afteryouremovetheselines,addthenextlines.Thecodefileforthesampleisincludedintherestful_python_chapter_10_02folder:
classApplication(web.Application):
def__init__(self,**kwargs):
handlers=[
(r"/hexacopters/([0-9]+)",AsyncHexacopterHandler),
(r"/leds/([0-9]+)",AsyncLedHandler),
(r"/altimeters/([0-9]+)",AsyncAltimeterHandler),
]
super(Application,self).__init__(handlers,**kwargs)
if__name__=="__main__":
application=Application()
application.listen(8888)
tornado_ioloop=ioloop.IOLoop.instance()
ioloop.PeriodicCallback(lambda:None,500,tornado_ioloop).start()
tornado_ioloop.start()
ThecodedeclaresanApplicationclass,specifically,asubclassoftornado.web.Applicationthatoverridestheinheritedconstructor,thatis,the__init__method.TheconstructordeclaresthehandlerslistthatmapsURLpatternstoasynchronousrequesthandlersandthencallstheinheritedconstructorwiththelistasoneofitsarguments.Wecreatetheclasstomakeitpossiblefortheteststousethisclass.
Then,themainmethodcreatesaninstanceoftheApplicationclass,registersaperiodiccallbackthatwillbeexecutedevery500millisecondsbytheIOLooptomakeitpossibletouseCtrl+CtostoptheHTTPserver,andfinallycallsthestartmethod.Theasync_api.pyscriptisgoingtocontinueworkinginthesameway.ThemaindifferenceisthatwecanreusetheApplicationclassinourtests.
Finally,createanewtextfilenamed.coveragercwithinthevirtualenvironment'srootfolderwiththefollowingcontent.Thecodefileforthesampleisincludedintherestful_python_chapter_10_02folder:
[run]
include=async_api.py,drone.py
Thisway,thecoverageutilitywillonlyconsiderthecodeintheasync_api.pyanddrone.pyfileswhenprovidinguswiththetestcoveragereport.Wewillhaveamoreaccuratetestcoveragereportwiththissettingsfile.
Tip
Inthiscase,wewon'tbeusingconfigurationfilesforeachenvironment.However,inmorecomplexapplications,youwilldefinitelywanttouseconfigurationfiles.
WritingafirstroundofunittestsNow,wewillwriteafirstroundofunittests.Specifically,wewillwriteunittestsrelatedtotheLEDresources.Createanewtestssubfolderwithinthevirtualenvironment'srootfolder.Then,createanewtest_hexacopter.pyfilewithinthenewtestssubfolder.AddthefollowinglinesthatdeclaremanyimportstatementsandtheTextHexacopterclass.Thecodefileforthesampleisincludedintherestful_python_chapter_10_02folder:
importunittest
importstatus
importjson
fromtornadoimportioloop,escape
fromtornado.testingimportAsyncHTTPTestCase,gen_test,gen
fromasync_apiimportApplication
classTestHexacopter(AsyncHTTPTestCase):
defget_app(self):
self.app=Application(debug=False)
returnself.app
deftest_set_and_get_led_brightness_level(self):
"""
EnsurewecansetandgetthebrightnesslevelsforbothLEDs
"""
patch_args_led_1={'brightness_level':128}
patch_args_led_2={'brightness_level':250}
patch_response_led_1=self.fetch(
'/leds/1',
method='PATCH',
body=json.dumps(patch_args_led_1))
patch_response_led_2=self.fetch(
'/leds/2',
method='PATCH',
body=json.dumps(patch_args_led_2))
self.assertEqual(patch_response_led_1.code,status.HTTP_200_OK)
self.assertEqual(patch_response_led_2.code,status.HTTP_200_OK)
get_response_led_1=self.fetch(
'/leds/1',
method='GET')
get_response_led_2=self.fetch(
'/leds/2',
method='GET')
self.assertEqual(get_response_led_1.code,status.HTTP_200_OK)
self.assertEqual(get_response_led_2.code,status.HTTP_200_OK)
get_response_led_1_data=escape.json_decode(get_response_led_1.body)
get_response_led_2_data=escape.json_decode(get_response_led_2.body)
self.assertTrue('brightness_level'inget_response_led_1_data.keys())
self.assertTrue('brightness_level'inget_response_led_2_data.keys())
self.assertEqual(get_response_led_1_data['brightness_level'],
patch_args_led_1['brightness_level'])
self.assertEqual(get_response_led_2_data['brightness_level'],
patch_args_led_2['brightness_level'])
TheTestHexacopterclassisasubclassoftornado.testing.AsyncHTTPTestCase,thatis,atestcasethatstartsupaTornadoHTTPServer.Theclassoverridestheget_appmethodthatreturnsthetornado.web.Applicationinstancethatwewanttotest.Inthiscase,wereturnaninstanceoftheApplicationclassdeclaredintheasync_apimodule,withthedebugargumentsettoFalse.
Thetest_set_and_get_led_brightness_levelmethodtestswhetherwecansetandgetthebrightnesslevelsforboththewhiteandblueLED.ThecodecomposesandsendstwoHTTPPATCHmethodstosetnewbrightnesslevelvaluesfortheLEDswhoseIDsareequalto1and2.ThecodesetsadifferentbrightnesslevelforeachLED.
Thecodecallstheself.fetchmethodtocomposeandsendtheHTTPPATCHrequestandcallsjson.dumpswiththedictionarytobesenttothebodyasanargument.Then,thecodeusesself.fetchagaintocomposeandsendtwoHTTPGETmethodstoretrievethebrightnesslevelvaluesfortheLEDswhosebrightnessvalueshavebeenmodified.Thecodeusestornado.escape.json_decodetoconvertthebytesintheresponsebodytoaPythondictionary.ThemethodusesassertEqualandassertTruetocheckforthefollowingexpectedresults:
Thestatus_codeforthetwoHTTPPATCHresponsesisHTTP200OK(status.HTTP_200_OK)Thestatus_codeforthetwoHTTPGETresponsesisHTTP200OK(status.HTTP_200_OK)TheresponsebodyforthetwoHTTPGETresponsesincludeakeynamedbrigthness_level
Thevalueforthebrightness_levelkeyintheHTTPGETresponsesareequaltothebrightnesslevelsettoeachLED
Runningunittestswithnose2andcheckingtestingcoverageNow,runthefollowingcommandtocreateallthenecessarytablesinourtestdatabaseandusethenose2testrunningtoexecuteallthetestswecreated.ThetestrunnerwillexecuteallthemethodsforourTestHexacopterclassthatstartwiththetest_prefixandwilldisplaytheresults.Inthiscase,wejusthaveonemethodthatmatchesthecriteria,butwewilladdmorelater.
Runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing.Wewillusethe-voptiontoinstructnose2toprinttestcasenamesandstatuses.The--with-coverageoptionturnsontestcoveragereportinggeneration:
nose2-v--with-coverage
Thefollowinglinesshowthesampleoutput.Noticethatthenumbersshowninthereportmighthavesmalldifferencesifourcodeincludesadditionallinesorcomments:
test_set_and_get_led_brightness_level(test_hexacopter.TestHexacopter)...
I'vestartedsettingtheBlueLED'sbrightnesslevel
I'vefinishedsettingtheBlueLED'sbrightnesslevel
I'vestartedsettingtheWhiteLED'sbrightnesslevel
I'vefinishedsettingtheWhiteLED'sbrightnesslevel
I'vestartedretrievingBlueLED'sstatus
I'vefinishedretrievingBlueLED'sstatus
I'vestartedretrievingWhiteLED'sstatus
I'vefinishedretrievingWhiteLED'sstatus
ok
----------------------------------------------------------------
Ran1testin1.311s
OK
-----------coverage:platformwin32,python3.5.2-final-0-----
NameStmtsMissCover
----------------------------------
async_api.py1296947%
drone.py571868%
----------------------------------
TOTAL1868753%
Bydefault,nose2looksformoduleswhosenamesstartwiththetestprefix.Inthiscase,theonlymodulethatmatchesthecriteriaisthetest_hexacoptermodule.Inthemodulesthatmatchthecriteria,nose2loadstestsfromallthesubclassesofunittest.TestCaseandthefunctionswhosenamesstartwiththetestprefix.Thetornado.testing.AsyncHTTPTestCaseincludesunittest.TestCaseasoneofitssuperclassesintheclasshierarchy.
Theoutputprovideddetailsindicatingthatthetestrunnerdiscoveredandexecutedonetestanditpassed.TheoutputdisplaysthemethodnameandtheclassnameforeachmethodintheTestHexacopterclassthatstartedwiththetest_prefixandrepresentedatesttobeexecuted.
Wedefinitelyhaveaverylowcoverageforasync_api.pyanddrone.pybasedonthemeasurementsshowninthereport.Infact,wejustwroteonetestrelatedtoLEDs,andtherefore,itmakessensethatthecoveragehastobeimproved.Wedidn'tcreatetestsrelatedtootherhexacopterresources.
Wecanrunthecoveragecommandwiththe-mcommand-lineoptiontodisplaythelinenumbersofthemissingstatementsinanewMissingcolumn:
coveragereport-m
Thecommandwillusetheinformationfromthelastexecutionandwilldisplaythemissingstatements.Thenextlinesshowasampleoutputthatcorrespondstothepreviousexecutionoftheunittests.Noticethatthenumbersshowninthereportmighthavesmalldifferencesifourcodeincludesadditionallinesorcomments:
NameStmtsMissCoverMissing
--------------------------------------------
async_api.py1296947%137-150,154,158-187,191,202-204,
226-228,233-235,249-256,270-282,286,311-315
drone.py571868%11-12,24,27-34,37,40-41,59,61,68-
69
--------------------------------------------
TOTAL1868753%
Now,runthefollowingcommandtogetannotatedHTMLlistingsdetailingmissedlines:
coveragehtml
Opentheindex.htmlHTMLfilegeneratedinthehtmlcovfolderwithyourWebbrowser.ThefollowingscreenshotshowsanexamplereportthatcoveragegeneratedinHTMLformat:
Clickortapondrony.pyandtheWebbrowserwillrenderaWebpagethatdisplaysthestatementsthatwererun,themissingones,andtheexcludedones,withdifferentcolors.Wecanclickortapontherun,missing,andexcludedbuttonstoshoworhidethebackgroundcolorthatrepresentsthestatusforeachlineofcode.Bydefault,themissinglinesofcodewillbedisplayedwithapinkbackground.Thus,wemustwriteunitteststhattargettheselinesofcodetoimproveourtestcoverage.
ImprovingtestingcoverageNow,wewillwriteadditionalunitteststoimprovethetestingcoverage.Specifically,wewillwriteunittestsrelatedtothehexacoptermotorandthealtimeter.Opentheexistingtest_hexacopter.pyfileandinsertthefollowinglinesafterthelastline.Thecodefileforthesampleisincludedintherestful_python_chapter_10_03folder:
deftest_set_and_get_hexacopter_motor_speed(self):
"""
Ensurewecansetandgetthehexacopter'smotorspeed
"""
patch_args={'motor_speed':700}
patch_response=self.fetch(
'/hexacopters/1',
method='PATCH',
body=json.dumps(patch_args))
self.assertEqual(patch_response.code,status.HTTP_200_OK)
get_response=self.fetch(
'/hexacopters/1',
method='GET')
self.assertEqual(get_response.code,status.HTTP_200_OK)
get_response_data=escape.json_decode(get_response.body)
self.assertTrue('speed'inget_response_data.keys())
self.assertTrue('turned_on'inget_response_data.keys())
self.assertEqual(get_response_data['speed'],
patch_args['motor_speed'])
self.assertEqual(get_response_data['turned_on'],
True)
deftest_get_altimeter_altitude(self):
"""
Ensurewecangetthealtimeter'saltitude
"""
get_response=self.fetch(
'/altimeters/1',
method='GET')
self.assertEqual(get_response.code,status.HTTP_200_OK)
get_response_data=escape.json_decode(get_response.body)
self.assertTrue('altitude'inget_response_data.keys())
self.assertGreaterEqual(get_response_data['altitude'],
0)
self.assertLessEqual(get_response_data['altitude'],
3000)
ThepreviouscodeaddedthefollowingtwomethodstotheTestHexacopterclasswhosenamesstartwiththetest_prefix:
test_set_and_get_hexacopter_motor_speed:Thistestswhetherwecansetandgetthehexacopter'smotorspeed.test_get_altimeter_altitude:Thistestswhetherwecanretrievethealtitudevaluefromthealtimeter.
Wejustcodedafewtestsrelatedtothehexacopterandthealtimeterinordertoimprovetestcoverageandnoticetheimpactonthetestcoveragereport.
Now,runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing:
nose2-v--with-coverage
Thefollowinglinesshowthesampleoutput.Noticethatthenumbersshowninthereportmighthavesmalldifferencesifourcodeincludesadditionallinesorcomments:
test_get_altimeter_altitude(test_hexacopter.TestHexacopter)...
I'vestartedretrievingthealtitude
I'vefinishedretrievingthealtitude
ok
test_set_and_get_hexacopter_motor_speed(test_hexacopter.TestHexacopter)...
I'vestartedsettingthehexacopter'smotorspeed
I'vefinishedsettingthehexacopter'smotorspeed
I'vestartedretrievinghexacopter'sstatus
I'vefinishedretrievinghexacopter'sstatus
ok
test_set_and_get_led_brightness_level(test_hexacopter.TestHexacopter)...
I'vestartedsettingtheBlueLED'sbrightnesslevel
I'vefinishedsettingtheBlueLED'sbrightnesslevel
I'vestartedsettingtheWhiteLED'sbrightnesslevel
I'vefinishedsettingtheWhiteLED'sbrightnesslevel
I'vestartedretrievingBlueLED'sstatus
I'vefinishedretrievingBlueLED'sstatus
I'vestartedretrievingWhiteLED'sstatus
I'vefinishedretrievingWhiteLED'sstatus
ok
--------------------------------------------------------------
Ran3testsin2.282s
OK
-----------coverage:platformwin32,python3.5.2-final-0---
NameStmtsMissCover
----------------------------------
async_api.py1293871%
drone.py57493%
----------------------------------
TOTAL1864277%
Theoutputprovideddetailsindicatingthatthetestrunnerexecuted3testsandallofthempassed.ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageincreasedtheCoverpercentageoftheasync_api.pymodulefrom47%inthepreviousrunto71%.Inaddition,thepercentageofthedrone.pymoduleincreasedfrom68%to93%becausewewroteteststhatworkedwithallthecomponentsforthedrone.Thenewadditionaltestswewroteexecutedadditionalcodeinthetwomodules,andtherefore,thereisanimpactinthecoveragereport.
Ifwetakealookatthemissingstatements,wewillnoticethatwearen'ttestingscenarioswherevalidationsfail.Now,wewillwriteadditionalunitteststoimprovethetestingcoveragefurther.Specifically,wewillwriteunitteststomakesurethatwecannotsetinvalidbrightness
levelsfortheLEDs,wecannotsetinvalidmotorspeedsforthehexacopter,andwereceiveanHTTP404NotFoundstatuscodewhenwetrytoaccessaresourcethatdoesn'texist.Opentheexistingtest_hexacopter.pyfileandinsertthefollowinglinesafterthelastline.Thecodefileforthesampleisincludedintherestful_python_chapter_10_04folder:
deftest_set_invalid_brightness_level(self):
"""
EnsurewecannotsetaninvalidbrightnesslevelforaLED
"""
patch_args_led_1={'brightness_level':256}
patch_response_led_1=self.fetch(
'/leds/1',
method='PATCH',
body=json.dumps(patch_args_led_1))
self.assertEqual(patch_response_led_1.code,status.HTTP_400_BAD_REQUEST)
patch_args_led_2={'brightness_level':-256}
patch_response_led_2=self.fetch(
'/leds/2',
method='PATCH',
body=json.dumps(patch_args_led_2))
self.assertEqual(patch_response_led_2.code,status.HTTP_400_BAD_REQUEST)
patch_response_led_3=self.fetch(
'/leds/2',
method='PATCH',
body=json.dumps({}))
self.assertEqual(patch_response_led_3.code,status.HTTP_400_BAD_REQUEST)
deftest_set_brightness_level_invalid_led_id(self):
"""
EnsurewecannotsetthebrightnesslevelforaninvalidLEDid
"""
patch_args_led_1={'brightness_level':128}
patch_response_led_1=self.fetch(
'/leds/100',
method='PATCH',
body=json.dumps(patch_args_led_1))
self.assertEqual(patch_response_led_1.code,status.HTTP_404_NOT_FOUND)
deftest_get_brightness_level_invalid_led_id(self):
"""
EnsurewecannotgetthebrightnesslevelforaninvalidLEDid
"""
patch_response_led_1=self.fetch(
'/leds/100',
method='GET')
self.assertEqual(patch_response_led_1.code,status.HTTP_404_NOT_FOUND)
deftest_set_invalid_motor_speed(self):
"""
Ensurewecannotsetaninvalidmotorspeedforthehexacopter
"""
patch_args_hexacopter_1={'motor_speed':89000}
patch_response_hexacopter_1=self.fetch(
'/hexacopters/1',
method='PATCH',
body=json.dumps(patch_args_hexacopter_1))
self.assertEqual(patch_response_hexacopter_1.code,
status.HTTP_400_BAD_REQUEST)
patch_args_hexacopter_2={'motor_speed':-78600}
patch_response_hexacopter_2=self.fetch(
'/hexacopters/1',
method='PATCH',
body=json.dumps(patch_args_hexacopter_2))
self.assertEqual(patch_response_hexacopter_2.code,
status.HTTP_400_BAD_REQUEST)
patch_response_hexacopter_3=self.fetch(
'/hexacopters/1',
method='PATCH',
body=json.dumps({}))
self.assertEqual(patch_response_hexacopter_3.code,
status.HTTP_400_BAD_REQUEST)
deftest_set_motor_speed_invalid_hexacopter_id(self):
"""
Ensurewecannotsetthemotorspeedforaninvalidhexacopterid
"""
patch_args_hexacopter_1={'motor_speed':128}
patch_response_hexacopter_1=self.fetch(
'/hexacopters/100',
method='PATCH',
body=json.dumps(patch_args_hexacopter_1))
self.assertEqual(patch_response_hexacopter_1.code,
status.HTTP_404_NOT_FOUND)
deftest_get_motor_speed_invalid_hexacopter_id(self):
"""
Ensurewecannotgetthemotorspeedforaninvalidhexacopterid
"""
patch_response_hexacopter_1=self.fetch(
'/hexacopters/5',
method='GET')
self.assertEqual(patch_response_hexacopter_1.code,
status.HTTP_404_NOT_FOUND)
deftest_get_altimeter_altitude_invalid_altimeter_id(self):
"""
Ensurewecannotgetthealtimeter'saltitudeforaninvalidaltimeter
id
"""
get_response=self.fetch(
'/altimeters/5',
method='GET')
self.assertEqual(get_response.code,status.HTTP_404_NOT_FOUND)
ThepreviouscodeaddedthefollowingsevenmethodstotheTestHexacopterclasswhosenamesstartwiththetest_prefix:
test_set_invalid_brightness_level:ThismakessurethatwecannotsetaninvalidbrightnesslevelforanLEDthroughanHTTPPATCHrequest.test_set_brightness_level_invalid_led_id:Thismakessurethatwecannotsetthe
brightnesslevelforaninvalidLEDIDthroughanHTTPPATCHrequest.test_get_brightness_level_invalid_led_id:ThismakessurethatwecannotgetthebrightnesslevelforaninvalidLEDID.test_set_invalid_motor_speed:ThismakessurethatwecannotsetaninvalidmotorseedforthehexacopterthroughanHTTPPATCHrequest.test_set_motor_speed_invalid_hexacopter_id:ThismakessurethatwecannotsetthemotorspeedforaninvalidhexacopterIDthroughanHTTPPATCHrequest.test_get_motor_speed_invalid_hexacopter_id:ThismakessurethatwecannotgetthemotorspeedforaninvalidhexacopterID.test_get_altimeter_altitude_invalid_altimeter_id:ThismakessurethatwecannotgetthealtitudevalueforaninvalidaltimeterID.
Wecodedmanyteststhatwillmakesurethatallthevalidationsworkasexpected.Now,runthefollowingcommandwithinthesamevirtualenvironmentwehavebeenusing:
nose2-v--with-coverage
Thefollowinglinesshowthesampleoutput.Noticethatthenumbersshowninthereportmighthavesmalldifferencesifourcodeincludesadditionallinesorcomments:
I'vefinishedretrievingthealtitude
ok
test_get_altimeter_altitude_invalid_altimeter_id
(test_hexacopter.TestHexacopter)...WARNING:tornado.access:404GET
/altimeters/5(127.0.0.1)1.00ms
ok
test_get_brightness_level_invalid_led_id(test_hexacopter.TestHexacopter)...
WARNING:tornado.access:404GET/leds/100(127.0.0.1)2.01ms
ok
test_get_motor_speed_invalid_hexacopter_id(test_hexacopter.TestHexacopter)
...WARNING:tornado.access:404GET/hexacopters/5(127.0.0.1)2.01ms
ok
test_set_and_get_hexacopter_motor_speed(test_hexacopter.TestHexacopter)...
I'vestartedsettingthehexacopter'smotorspeed
I'vefinishedsettingthehexacopter'smotorspeed
I'vestartedretrievinghexacopter'sstatus
I'vefinishedretrievinghexacopter'sstatus
ok
test_set_and_get_led_brightness_level(test_hexacopter.TestHexacopter)...
I'vestartedsettingtheBlueLED'sbrightnesslevel
I'vefinishedsettingtheBlueLED'sbrightnesslevel
I'vestartedsettingtheWhiteLED'sbrightnesslevel
I'vefinishedsettingtheWhiteLED'sbrightnesslevel
I'vestartedretrievingBlueLED'sstatus
I'vefinishedretrievingBlueLED'sstatus
I'vestartedretrievingWhiteLED'sstatus
I'vefinishedretrievingWhiteLED'sstatus
ok
test_set_brightness_level_invalid_led_id(test_hexacopter.TestHexacopter)...
WARNING:tornado.access:404PATCH/leds/100(127.0.0.1)1.01ms
ok
test_set_invalid_brightness_level(test_hexacopter.TestHexacopter)...I've
startedsettingtheBlueLED'sbrightnesslevel
I'vefailedsettingtheBlueLED'sbrightnesslevel
WARNING:tornado.access:400PATCH/leds/1(127.0.0.1)13.51ms
I'vestartedsettingtheWhiteLED'sbrightnesslevel
I'vefailedsettingtheWhiteLED'sbrightnesslevel
WARNING:tornado.access:400PATCH/leds/2(127.0.0.1)10.03ms
WARNING:tornado.access:400PATCH/leds/2(127.0.0.1)2.01ms
ok
test_set_invalid_motor_speed(test_hexacopter.TestHexacopter)...I'vestarted
settingthehexacopter'smotorspeed
I'vefailedsettingthehexacopter'smotorspeed
WARNING:tornado.access:400PATCH/hexacopters/1(127.0.0.1)19.27ms
I'vestartedsettingthehexacopter'smotorspeed
I'vefailedsettingthehexacopter'smotorspeed
WARNING:tornado.access:400PATCH/hexacopters/1(127.0.0.1)9.04ms
WARNING:tornado.access:400PATCH/hexacopters/1(127.0.0.1)1.00ms
ok
test_set_motor_speed_invalid_hexacopter_id(test_hexacopter.TestHexacopter)
...WARNING:tornado.access:404PATCH/hexacopters/100(127.0.0.1)1.00ms
ok
----------------------------------------------------------------------
Ran10testsin5.905s
OK
-----------coverage:platformwin32,python3.5.2-final-0-----------
NameStmtsMissCover
----------------------------------
async_api.py129596%
drone.py570100%
----------------------------------
TOTAL186597%
Theoutputprovideddetailsindicatingthatthetestrunnerexecuted10testsandallofthempassed.ThetestcodecoveragemeasurementreportprovidedbythecoveragepackageincreasedtheCoverpercentageoftheasync_api.pymodulefrom71%inthepreviousrunto97%.Inaddition,thepercentageofthedrone.pymoduleincreasedfrom93%to100%.Ifwecheckthecoveragereport,wewillnoticethattheonlystatementsthataren'texecutedarethestatementsincludedinthemainmethodfortheasync_api.pymodulebecausetheyaren'tpartofthetests.Thus,wecansaythatwehave100%coverage.
Nowthatwehaveagreattestcoverage,wecangeneratetherequirements.txtfilethatliststheapplicationdependenciestogetherwiththeirversions.Thisway,anyplatforminwhichwedecidetodeploytheRESTfulAPIwillbeabletoeasilyinstallallthenecessarydependencieslistedinthefile.
Runthefollowingpipfreezetogeneratetherequirements.txtfile:
pipfreeze>requirements.txt
Thefollowinglinesshowthecontentofasamplegeneratedrequirements.txtfile.However,bearinmindthatmanypackagesincreasetheirversionnumberquicklyandyoumightseedifferentversionsinyourconfiguration:
cov-core==1.15.0
coverage==4.2
nose2==0.6.5
six==1.10.0
tornado==4.4.1
OtherPythonWebframeworksforbuildingRESTfulAPIsWebuiltRESTfulWebServiceswithDjango,Flask,andTornado.However,PythonhasmanyotherWebframeworksthatarealsosuitableforbuildingRESTfulAPIs.Everythingwelearnedthroughoutthebookaboutdesigning,building,testing,anddeployingaRESTfulAPIisalsoapplicabletoanyotherPythonWebframeworkwedecidetouse.ThefollowinglistenumeratesadditionalframeworksandtheirmainWebpage:
Pyramid:http://www.pylonsproject.org/Bottle:http://bottlepy.org/Falcon:https://falconframework.org/
AsalwayshappenswithanyPythonWebframework,thereareadditionalpackagesthatmightsimplifyourmostcommontasks.Forexample,itispossibletouseRamsesincombinationwithPyramidtocreateRESTfulAPIsbyworkingwithRAML(RESTfulAPIModelingLanguage),whosespecificationisavailableathttp://github.com/raml-org/raml-spec.YoucanreadmoredetailsaboutRamsesathttp://ramses.readthedocs.io/en/stable/getting_started.html.
Testyourknowledge1. Theconcurrent.futures.ThreadPoolExecutorclassprovidesus:
1. Ahigh-levelinterfaceforsynchronouslyexecutingcallables.2. Ahigh-levelinterfaceforasynchronouslyexecutingcallables.3. Ahigh-levelinterfaceforcomposingHTTPrequests.
2. [email protected]_on_executordecoratorallowsusto:1. Runanasynchronousmethodsynchronouslyonanexecutor.2. RunanasynchronousmethodonanexecutorwithoutgeneratingaFuture.3. Runasynchronousmethodasynchronouslyonanexecutor.
3. TherecommendedwaytowriteasynchronouscodeinTornadoistouse:1. Coroutines.2. Chainedcallbacks.3. Subroutines.
4. Thetornado.Testing.AsyncHTTPTestCaseclassrepresents:1. AtestcasethatstartsupaFlaskHTTPServer.2. AtestcasethatstartsupaTornadoHTTPServer.3. Atestcasethatdoesn'tstartupanyHTTPServer.
5. IfwewanttoconvertthebytesinaJSONresponsebodytoaPythondictionary,wecanusethefollowingfunction:1. tornado.escape.json_decode2. tornado.escape.byte_decode3. tornado.escape.response_body_decode
SummaryInthischapter,weunderstoodthedifferencebetweensynchronousandasynchronousexecution.WecreatedanewversionoftheRESTfulAPIthattakesadvantageofthenon-blockingfeaturesinTornadocombinedwithasynchronousexecution.WeimprovedscalabilityforourexistingAPIandwemadeitpossibletostartexecutingotherrequestswhilewaitingfortheslowI/Ooperationswithsensorsandactuators.Weavoidedsplittingourmethodsintomultiplemethodswithcallbacksbyusingthetornado.gengenerator-basedinterfacethatTornadoprovidestomakeiteasiertoworkinanasynchronousenvironment.
Then,wesetupatestingenvironment.Weinstallednose2tomakeiteasytodiscoverandexecuteunittests.Wewroteafirstroundofunittests,measuredtestcoverage,andthenwewroteadditionalunitteststoimprovetestcoverage.Wecreatedallthenecessaryteststohaveacompletecoverageofallthelinesofcode.
WebuiltRESTfulWebServiceswithDjango,Flask,andTornado.Wehavechosenthemostappropriateframeworkforeachcase.WelearnedtodesignaRESTfulAPIfromscratchandtorunallthenecessaryteststomakesurethatourAPIworkswithoutissuesaswereleasenewversions.Now,wearereadytocreateRESTfulAPIswithanyoftheWebframeworkswithwhomwehavebeenworkingthroughoutthisbook.
Chapter11.ExerciseAnswers
Chapter1,DevelopingRESTfulAPIswithDjango
Q1 2
Q2 1
Q3 3
Q4 1
Q5 3
Chapter2,WorkingwithClass-BasedViewsandHyperlinkedAPIsinDjango
Q1 1
Q2 2
Q3 3
Q4 3
Q5 1
Chapter3,ImprovingandAddingAuthenticationtoanAPIWithDjango
Q1 3
Q2 1
Q3 2
Q4 1
Q5 3
Chapter4,Throttling,Filtering,Testing,andDeployinganAPIwithDjango
Q1 2
Q2 1
Q3 3
Q4 1
Q5 1
Chapter5,DevelopingRESTfulAPIswithFlask
Q1 1
Q2 3
Q3 3
Q4 2
Q5 1
Chapter6,WorkingwithModels,SQLAlchemy,andHyperlinkedAPIsinFlask
Q1 1
Q2 2
Q3 3
Q4 3
Q5 1
Chapter7,ImprovingandAddingAuthenticationtoanAPIwithFlask
Q1 3
Q2 1
Q3 3
Q4 1
Q5 2
Chapter8,TestingandDeployinganAPIwithFlask
Q1 1
Q2 2
Q3 1
Q4 1
Q5 3
Chapter9,DevelopingRESTfulAPIswithTornado
Q1 2
Q2 1
Q3 3
Q4 3
Q5 2
Chapter10,WorkingwithAsynchronousCode,Testing,andDeployinganAPIwithTornado
Q1 2
Q2 3
Q3 1
Q4 2
Q5 1