Tutorial: getting started with the Personal Factory API in Python


Personal Factory is a cloud software platform for app developers to connect users directly to manufacturing devices to make custom goods on-demand.

It integrates product creation and customization apps with an established manufacturing and distribution system, so users can turn their designs into final products and have those products delivered to their door.

This is part one in a series of tutorials written by technologist Mark Schafer on working with the Personal Factory API in Python.

The full API documentation is here. All supplied examples use cURL.

The code for this tutorial can be found here: https://github.com/Neon22/Ponoko-API-from-python

Basic steps

Firstly we need to work out how to communicate with the sandbox created for us to play in without mucking up the proper site. To do that we have to use an authentication method to prove we are a legitimate user of the API.

There are three ways to authenticate yourself with Ponoko. Two of them are for using it yourself and the third allows you to add designs and products to other people’s (your clients?) accounts. This third method uses OAuth as the authentication method. We won’t be dealing with that here. Instead we will just be using the simple personal method.

We will use the “Simple access keys (for your own Personal Factory only)” which shows up under that heading in your application page.

All of this assumes you have registered an application with Ponoko and can see it under your “my apps” menu item on your personal page. This is where you will find these keys.

The API documentation page shows us a simple url showing how we can send a message to Ponoko to discover the manufacturing nodes available for us to use to create products. Onto this simple URL we need to add the authentication args.

Our simple authentication string is in this form (using your own values shown in your app page) looks like this:

app_key=abcdefgh&user_access_key=stuvwxyz

The basic url looks like this:

https://sandbox.ponoko.com/services/api/v2/nodes

Resulting in this:

https://sandbox.ponoko.com/services/api/v2/nodes?app_key=abcdefgh&user_access_key=stuvwxyz

We expect to get back a json format string containing useful info (defined one the API page). So lets try this in Python.

Keys

Firstly – our secret app keys (on your My apps page)

1 2
app_key = ("app_key", " abcdefgh ")
user_access_key = ("user_access_key", " stuvwxyz ")
view raw gistfile1.py hosted with ❤ by GitHub

Sandbox vs Live

Secondly – we need to play in the safe sandbox until we get everything working. Then we’re going to want to switch to live. So we may as well make this easy now. The difference between the two is ‘www’ or ‘sandbox’ at the head of the URL.

1 2 3 4
# Use live system or Sandbox
#live_or_test = "www"
live_or_test = "sandbox"
baseURL = "http://"+ live_or_test +".ponoko.com/services/api/v2"
view raw gistfile1.py hosted with ❤ by GitHub

We will switch comments when we go live.

Python – urllib2 and pycurl

In python there are several good ways to construct, send, and receive this information. The one I’m going to use is the urllib2 module. Later we will use pycurl for posting more complex multi-part forms.

In python we have:

1 2 3 4 5 6 7 8 9 10 11 12 13 14
from urllib2 import Request, urlopen
from urllib import urlencode, quote
import json
 
nodesTAG = "/nodes" # added onto baseURL
 
# place following function code in here
 
## main
if __name__ == "__main__":#
keydata = urlencode((app_key, user_access_key))
success, nodes = get_manufacturing_nodes(baseURL, keydata)
if success:
print success, nodes
view raw gistfile1.py hosted with ❤ by GitHub

This approach has the function get_manufacturing_nodes returning two values. Success and the nodes found. Many things can go wrong in http communications so we need to address that.

Lets define that function:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
def get_manufacturing_nodes(URL, keydata, verbose=True):
""" Get the nodes from dedicated url.
Uses basic request
"""
URL_full = "%s%s?%s" % (URL, nodesTAG, keydata)
if verbose:
print "Getting Node catalog\n %s" % (URL_full)
req = Request(URL_full)
success, result = basic_request(req)
if not success:
# fail!!
if verbose: print "Error:", result
return (False, result)
else: # Success
nodes = json.loads(result[0])['nodes']
if verbose:
print "Response:\n %s" % (nodes)
# for each node
for i in range(len(nodes)):
print "Node name", nodes[i][u'name']
print " Last updated:", nodes[i][u'materials_updated_at']
print " node_key", nodes[i][u'key']
return (True, nodes)
view raw gistfile1.py hosted with ❤ by GitHub

In the above function:

  1. We call it with the baseURL and the authentication key. We set verbose=True for debugging purposes. Later we’ll change this default to False.
  2. We construct the URL arguments manually as they are simple.
  3. Request has been imported from the urrllib2 module and formulates the request for us. When we call basic_request it will be actioned.
  4. The result is a success flag an dthe returned string from the Ponoko server.
  5. If the call failed we will see an error message in the result.
  6. If we succeeded then we wil hav the list of manufacturing nodes in json form in the result variable.
  7. So we extract them and report salient info on name and age for each manufacturing node if verbose.

Here is the function to perform basic http requests using urllib2. It detects the various exceptions likely to occur and returns correct codes. It also has its own verbose flag – which you can use to help see what it is doing or debug it.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
def basic_request(request, verbose=False):
""" request is in form of a urllib2.Request()
Return (False, error) OR
(True, response, url, info)
"""
error = False
result = []
try:
response = urlopen(request,timeout=100)
except IOError, e:
if hasattr(e,'reason'):
error = ("FAILED to reach server", e.reason)
elif hasattr(e, 'code'):
error = ("FAIL - Server could not fulfill request", e.code)
except Exception as inst:
error = ("unspecified error occurred", inst.args)
else: # all good
result = (response.read(), response.geturl(), response.info())
if verbose:
print "Requested:\n %s" % request
print "Response:\n %s" % result
return (True, result)
# Failed - return error codes
if verbose:
print "Error", error
return (False, error)
view raw gistfile1.py hosted with ❤ by GitHub

It returns the error if it fails and a triplet of response, url, and info if successful. These may be useful for other purposes later. Primarily the response is all we need.

If we run this we should see a result like this:

1 2 3 4 5 6 7
Getting Node catalog
http://sandbox.ponoko.com/services/api/v2/nodes?app_key=abcdefgh &user_access_key=stuvwxyz
Response:
[{u'materials_updated_at': u'2011/06/27 20:23:16 +0000', u'name': u'Ponoko - United States', u'key': u'2e9d8ccdb04a'}]
Node name Ponoko - United States
Last updated: 2011/06/27 20:23:16 +0000
node_key 2e9d8ccdb04a
view raw gistfile1.sh hosted with ❤ by GitHub

If we had an error we might see this:

1 2 3
Getting Node catalog
http://sandbox.ponoko.com/services/api/v2/nodes?app_key= abcdefgh &user_access_key= stuvwxyz
Error: ('FAIL - Server could not fulfill request', 401)
view raw gistfile1.sh hosted with ❤ by GitHub

Showing us the only node in the sandbox is the USA hub, and when its materials were last updated. Once again – referring to the API docs is important to understand what to expect and what these fields are for.

We will use the last updated date to help us determine if we have to download a new materials catalog. If we did we could use a routine like the following to retrieve the product catalog to update our local store. (Which we are probably caching in our database.)

So we could change main to be:

1 2 3 4 5 6 7 8
## main
if __name__ == "__main__":#
keydata = urlencode((app_key, user_access_key))
success, nodes = get_manufacturing_nodes(baseURL, keydata)
if success:
print success, nodes
for i in range(len(nodes)):
update_node_materials(nodes[i], baseURL, keydata)
view raw gistfile1.py hosted with ❤ by GitHub

Also lets add a dummy materials structure to check against. This would instead come from your database and you will have to make code changes to reflect the different structure.

1 2 3 4 5 6 7 8 9 10 11 12 13
Node_materials = {'Ponoko - United States': [u'2010/06/28 20:23:16',
{'6bb50fb04a' :
{u'material_type': u'printed',
u'updated_at': u'2011/06/27 20:23:16 +0000',
u'key': u'6bb500cdb04a',
u'name': u'Durable plastic'}},
{'6b62cdbed4a' :
{u'material_type': u'printed',
u'updated_at': u'2011/06/27 20:23:16 +0000',
u'key': u'6bb9552cdb04a',
u'name': u'Rainbow Ceramic'}}
]
}
view raw gistfile1.py hosted with ❤ by GitHub

You’ll also need to add this near the others at the top of the file:

1 2 3
material_catalogTAG = "/material_catalog/" # added onto nodesTAG
from datetime import datetime
from copy import deepcopy
view raw gistfile1.py hosted with ❤ by GitHub

So for each node we can check to see if it needs updating. So update_node_materials might look like this:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
def update_node_materials(node, URL, keydata, verbose=True):
""" for a manufacturing node:
- retrieve the catalog in json format
- convert it to python dictionary and list structures
"""
location = node[u'name']
date = node[u'materials_updated_at']
key = node[u'key']
date = date[:-6] # strip off seconds
recorded_materials = Node_materials[location]
ourdate = datetime.strptime(recorded_materials[0], "%Y/%m/%d %H:%M:%S")
newdate = datetime.strptime(date, "%Y/%m/%d %H:%M:%S")
if verbose: print "Checking: %s vs. %s" % (ourdate, newdate)
if ourdate <= newdate:
# need to update some or all materials
catalog = recorded_materials[1]
catalogURL = "%s%s%s" % (URL, nodesTAG, material_catalogTAG)
request = "%s%s?%s" %(catalogURL, key, keydata)
if verbose: print "Requesting Catalog updates for %s\n %s" % (location, request)
req = Request(request)
result = basic_request(req)
if not result[0]:
# fail
print result[1]
else: # Success
materials = json.loads(result[1][0])
show_materials(materials, location)
# update the local store
#!!!
else: # no need to update
print "No change to materials for %s" % (location)
view raw gistfile1.py hosted with ❤ by GitHub

I’ve added a function called show_materials here to show what’s in the catalog and to save it. But you will need to update your own materials datastructure.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
def show_materials(materials, location):
""" show what's in the materials catalog """
key_superset = []
key_commonset = []
#
key = materials['key']
count = materials['count']
catalog = materials['materials']
if count != len(catalog):
# something not right
print "Expecting count to be same as length of catalog"
print "Failing..."
else: # good
print "Location: %s" % (location)
print " %s materials found" % (count)
for c in catalog:
keys = c.keys()
keys.sort()
# collect superset of all keys
for k in keys:
if k not in key_superset:
key_superset.append(k)
#
print " ", c['name']
if c['name'] == 'Cork':
print c['key'], c['weight'], c['thickness']
## if u'weight' in keys:
## print c['name']
## print " ", c
## #print " ",c['type']
## #print " ",c['kind']
## #print " ",c['updated_at']
## #print " ", keys
## if u'type' not in keys:
## print c['name']
## print " ", c
# Find common set of keys
key_commonset = deepcopy(key_superset)
for c in catalog:
keys = c.keys()
for k in key_commonset:
if k not in keys:
key_commonset.remove(k)
#
print "Superset of all keys found is:", key_superset
print "Common set of keys is:", key_commonset
view raw gistfile1.py hosted with ❤ by GitHub

There’s a lot of data in the catalog – so I have chosen to only print out the names and some info about the cork materials. I also gathered the keys that were used. This might help me when designing a database to know which fields are mandatory. Other possible fields are commented out above. Consult the API docs for all of them.

We would want to use the updated_at field to help us only modify fields in our material structure that were out of date.

For questions about this tutorial or any questions about using the Personal Factory API, please visit the Ponoko Developer Forum.

Part 2 in this tutorial series: Uploading a product.


Mark Schafer is a technologist at Wireframe Ltd.
He has done some stuff. Like:
• wrote the first commercial single skinned skeletal animation system.
• worked in features, commercials, video games, and tech companies.
• motion captured Gollum for the LOTR films.
• been in a few startups (UK, USA, NZ), some more successful than others…
• makes Neon light sculptures… amongst other things.
• is building a 1m cubed 3D printer using icing sugar.
• has a sparsely populated store in the Ponoko showroom.

Related posts:

No Responses to “Tutorial: getting started with the Personal Factory API in Python”

Comments are closed.