Last week I wrote about creating a web API using Windows Azure, ASP.Net and C#.
This week we're going to plug this into our original pi passport class: as we have a layered system, this should be as simple as changing the data layer.
To recap, 2 weeks ago we had this:
Which pretty much does everything using 2 xml files: One which holds all data about an Achievement, and another which holds data about the person.
Now that we have a web API, people will be stored on the internet, Achievement descriptions will be stored up there, but achievement questions and answers will be on the pi itself: this is because from the interwebs we don't actually need to know what the user did to win the achievement, just that they've won it, and maybe a little info about what it is. This does however make it a bit messy, as it's important to make sure IDs match up with the server or else we could have achievement IDs that are different on the pi to what they're referenced as on the server.
So here we need to:
- Change Save/Load people methods to use requests
- Change Save/Load achievements to use requests, and within this match IDs together properly
- Change the params of the class to be URLs to apis we need to call.
- Change the read/admin files slightly so that the params for class creation are correct.
Let's start with LoadPi:
def LoadPi(self): dom=self.Load(self.pi,"piSyst") try: pi_tag=dom.getElementsByTagName("pi")[0] except: self.SavePi() dom=self.Load(self.pi,"piSyst") pi_tag=dom.getElementsByTagName("pi")[0] a_q=requests.get(self.pi_url) a_data=a_q.json() a_dict={} for a in a_data: a_dict[a["ID"]]={"Description":a["Description"]} pid=pi_tag.getAttribute("ID") a_tags=pi_tag.getElementsByTagName("achievement") achievements={} if int(pid) in a_dict.keys(): achievements[pid]={"question":None,"answers":None,"Description":a_dict[int(pid)]["Description"]} else: desc=raw_input("Please enter a description for this pi:") post=requests.post(self.pi_url,data=json.dumps({"Description":desc}),headers={"Content-type":"application/json"}) id=str(int(post.json()["ID"])-1) achievements[id]={"question":None,"answers":None,"Description":desc} pi_tag.setAttribute("ID",str(id)) file=open(self.pi,'w') dom.writexml(file) a_q=requests.get(self.pi_url) a_data=a_q.json() a_dict={} for a in a_data: a_dict[a["ID"]]={"Description":a["Description"]} for a in a_tags: id=a.getAttribute("ID") qtag=a.getElementsByTagName("question")[0] question=qtag.childNodes[0].data atag=a.getElementsByTagName("answer") answers=[] for an in atag: answers.append(an.childNodes[0].data) achievements[id]={"question":question,"answers":answers,"Description":a_dict[id]["Description"]} return achievements
You will need to install requests, which is similar to urllib but a heck of a lot easier to understand: you can get this from PyPip using:
sudo pip install requests
Taking this section by section:
dom=self.Load(self.pi,"piSyst") try: pi_tag=dom.getElementsByTagName("pi")[0] except: self.SavePi() dom=self.Load(self.pi,"piSyst") pi_tag=dom.getElementsByTagName("pi")[0] a_q=requests.get(self.pi_url) a_data=a_q.json() a_dict={} for a in a_data: a_dict[a["ID"]]={"Description":a["Description"]} pid=pi_tag.getAttribute("ID") a_tags=pi_tag.getElementsByTagName("achievement") achievements={} if int(pid) in a_dict.keys(): achievements[pid]={"question":None,"answers":None,"Description":a_dict[int(pid)]["Description"]}
This first bit:
- Loads up the dom and tries to find the pi tag
- If it's not there, saves the pi creating the dom tag and making sure it's there for next use
- makes a request to the appropriate API asking for all of the achievements
- loads the JSON object into a python dictionary
- checks for the pi achievement on the server and creates an achievement instance holding the deets of this achievement
else: desc=raw_input("Please enter a description for this pi:") post=requests.post(self.pi_url,data=json.dumps({"Description":desc}),headers={"Content-type":"application/json"}) id=str(int(post.json()["ID"])-1) achievements[id]={"question":None,"answers":None,"Description":desc} pi_tag.setAttribute("ID",str(id)) file=open(self.pi,'w') dom.writexml(file) a_q=requests.get(self.pi_url) a_data=a_q.json() a_dict={} for a in a_data: a_dict[a["ID"]]={"Description":a["Description"]} for a in a_tags: id=a.getAttribute("ID") qtag=a.getElementsByTagName("question")[0] question=qtag.childNodes[0].data atag=a.getElementsByTagName("answer") answers=[] for an in atag: answers.append(an.childNodes[0].data) achievements[id]={"question":question,"answers":answers,"Description":a_dict[id]["Description"]} return achievements
Continuing, this section:
- Discovers the pi tag is not listed in the server's achievements
- it then asks you to create it, by giving the achievement a description and sends this to the server using a post request
- In order to make sure things match, it takes the ID from the server and puts this both in the achievement dictionary, and the pi.xml file
- It then asks for a list of achievements, creates them as a dictionary
- then it goes to the XML file and pulls out all the local achievements, and pulls their descriptions from the server's achievement list.
Before we forget, let's change the initialiser of the class:
def __init__(self,pifile,people_url,pi_url,link_url): self.pi=pifile self.pi_url=pi_url self.people_api=people_url self.link_api=link_url self.achievements=self.LoadPi() self.people=self.LoadPeople()
So here we still have the pi file, which is an xml thingymagig holding all the data about each achievement question, but we now have 3 API links: one for achievements (pi_url), one for people (people_api) and one for links (link_api).
Going back to SavePi:
Similarly: here we save all the achievements to the pi file and ensure they match up to a place on the server. We do this by loading all the achievements on the server, and checking whether they exist in the achievement dictionary. If they're on the server, not the dict, delete from the server, or create if the opposite is true.
Moving on we'll do SavePeople:
Now instead of using an xml file, we:
- Open up a connection to the people api
- Load them all up
- connect to the links api
- pull out the achievements for the people we just loaded
- compare the current list of people to this list
- if some people aren't in the server list, add them using post requests to both APIs
we're completely ignoring whether people are in the server list, but not the current list, because it's assumed they'll never be deleted unless by a system override.
Aaand lastly LoadPeople:
Which is much the same as the process described above, but in reverse.
Lastly, we need to modify read.py and admin.py to include the changes to the class definitions. This is simply a case of changing the initial NFC class call:
self=NFC('pi.xml','http://someurl.com/api/People','http://someurl.com/api/Achievements','http://someurl.com/api/Joins')
Aaand now we should be all done, and when you run read.py you should see each person get added to the database!