Building RESTful Python Web Servicesenglishonlineclub.com/pdf/Building RESTful Python...for...

Preview:

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,simplye-mailfeedback@packtpub.com,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,youcancontactusatquestions@packtpub.com,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','kevin@example.com','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,weapplythe@auth.login_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.

Weaddedthe@gen.coroutinedecoratortothegetmethod.Inaddition,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. The@tornado.concurrent.run_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

Recommended