3333
3434import urlparse
3535
36- __version__ = '0.3.1 '
36+ __version__ = '0.4.0 '
3737
3838REQUEST_TOKEN_URL = 'https://sso.openx.com/api/index/initiate'
3939ACCESS_TOKEN_URL = 'https://sso.openx.com/api/index/token'
4040AUTHORIZATION_URL = 'https://sso.openx.com/login/process'
41- API_PATH = '/ox/3.0'
42- HTTP_METHOD_OVERRIDES = ['DELETE' , 'PUT' ]
41+ API_PATH_V1 = '/ox/3.0'
42+ API_PATH_V2 = '/ox/4.0'
43+ ACCEPTABLE_PATHS = (API_PATH_V1 , API_PATH_V2 )
44+ JSON_PATHS = (API_PATH_V2 ,)
45+ HTTP_METHOD_OVERRIDES = ['DELETE' , 'PUT' , 'OPTIONS' ]
46+
47+ class UnknownAPIFormatError (ValueError ):
48+ """Client is passed an unrecognized API path that it cannot handle."""
49+ pass
4350
4451class Client (object ):
52+ """Client for making requests to the OX3 API. Maintains
53+ authentication and points all requests at a domain+path
54+ combination. Handles request and response data in the form
55+ of Python dictionaries, translated to and from the JSON and
56+ query string encoding the API itself uses.
57+
58+ """
4559
4660 def __init__ (self , domain , realm , consumer_key , consumer_secret ,
4761 callback_url = 'oob' ,
4862 scheme = 'http' ,
4963 request_token_url = REQUEST_TOKEN_URL ,
5064 access_token_url = ACCESS_TOKEN_URL ,
5165 authorization_url = AUTHORIZATION_URL ,
52- api_path = API_PATH ,
66+ api_path = API_PATH_V1 ,
5367 email = None ,
5468 password = None ,
5569 http_proxy = None ,
@@ -78,6 +92,14 @@ def __init__(self, domain, realm, consumer_key, consumer_secret,
7892 self .access_token_url = access_token_url
7993 self .authorization_url = authorization_url
8094 self .api_path = api_path
95+
96+ # Validate API path:
97+ if api_path not in ACCEPTABLE_PATHS :
98+ msg = '"{}" is not a recognized API path.' .format (api_path )
99+ msg += '\n Legal paths include:'
100+ for i in ACCEPTABLE_PATHS :
101+ msg += '\n {}' .format (i )
102+ raise UnknownAPIFormatError (msg )
81103
82104 # These get cleared after log on attempt.
83105 self ._email = email
@@ -135,15 +157,20 @@ def _sign_request(self, req):
135157 return \
136158 urllib2 .Request (req .get_full_url (), headers = req .headers , data = data )
137159
138- def request (self , url , method = 'GET' , headers = {}, data = None , sign = False ):
160+ def request (self , url , method = 'GET' , headers = {}, data = None , sign = False ,
161+ send_json = False ):
139162 """Helper method to make a (optionally OAuth signed) HTTP request."""
140163
141164 # Since we are using a urllib2.Request object we need to assign a value
142165 # other than None to "data" in order to make the request a POST request,
143166 # even if there is no data to post.
144- if method == 'POST' and not data :
167+ if method in ( 'POST' , 'PUT' ) and not data :
145168 data = ''
146169
170+ # If we're sending a JSON blob, we need to specify the header:
171+ if method in ('POST' , 'PUT' ) and send_json :
172+ headers ['Content-Type' ] = 'application/json'
173+
147174 req = urllib2 .Request (url , headers = headers , data = data )
148175
149176 # We need to set the request's get_method function to return a HTTP
@@ -156,11 +183,19 @@ def request(self, url, method='GET', headers={}, data=None, sign=False):
156183
157184 # Stringify data.
158185 if data :
159- # Everything needs to be UTF-8 for urlencode:
186+ # Everything needs to be UTF-8 for urlencode and json :
160187 data_utf8 = req .get_data ()
161188 for i in data_utf8 :
162- data_utf8 [i ] = data_utf8 [i ].encode ('utf-8' )
163- req .add_data (urllib .urlencode (data_utf8 ))
189+ # Non-string ints don't have encode and can
190+ # be handled by json.dumps already:
191+ try :
192+ data_utf8 [i ] = data_utf8 [i ].encode ('utf-8' )
193+ except AttributeError :
194+ pass
195+ if send_json :
196+ req .add_data (json .dumps (data_utf8 ))
197+ else :
198+ req .add_data (urllib .urlencode (data_utf8 ))
164199
165200 # In 2.4 and 2.5, urllib2 throws errors for all non 200 status codes.
166201 # The OpenX API uses 201 create responses and 204 for delete respones.
@@ -255,12 +290,15 @@ def validate_session(self):
255290
256291 self ._cookie_jar .set_cookie (cookie )
257292
258- url = '%s://%s%s/a/session/validate' % (self .scheme ,
259- self .domain ,
260- self .api_path )
293+ # v2 doesn't need this extra step, just the cookie:
294+ if self .api_path == API_PATH_V1 :
295+ url_format = '%s://%s%s/a/session/validate'
296+ url = url_format % (self .scheme ,
297+ self .domain ,
298+ self .api_path )
261299
262- res = self .request (url = url , method = 'PUT' )
263- return res .read ()
300+ res = self .request (url = url , method = 'PUT' )
301+ return res .read ()
264302
265303 def logon (self , email = None , password = None ):
266304 """Returns self after authentication.
@@ -280,11 +318,20 @@ def logon(self, email=None, password=None):
280318
281319 def logoff (self ):
282320 """Returns self after deleting authenticated session."""
283- self .delete ('/a/session' )
321+ if self .api_path == API_PATH_V1 :
322+ self .delete ('/a/session' )
323+ elif self .api_path == API_PATH_V2 :
324+ self .delete ('/session' )
325+ else :
326+ raise UnknownAPIFormatError (
327+ 'Unrecognized API path: %s' % self .api_path )
284328 return self
285329
286330 def _resolve_url (self , url ):
287- """"""
331+ """Converts an API path shorthand into a full URL unless
332+ given a full url already.
333+
334+ """
288335 parse_res = urlparse .urlparse (url )
289336
290337 # 2.4 returns a tuple instead of ParseResult. Since ParseResult is a
@@ -301,25 +348,53 @@ def _resolve_url(self, url):
301348 return url
302349
303350 def get (self , url ):
304- """"""
351+ """Issue a GET request to the given URL or API shorthand
352+
353+ """
305354 res = self .request (self ._resolve_url (url ), method = 'GET' )
306355 return json .loads (res .read ())
356+
357+ def options (self , url ):
358+ """Send a request with HTTP method OPTIONS to the given
359+ URL or API shorthand.
360+
361+ OX3 v2 uses this method for showing help information.
362+
363+ """
364+ res = self .request (self ._resolve_url (url ), method = 'OPTIONS' )
365+ return json .loads (res .read ())
366+
367+ def put (self , url , data = None ):
368+ """Issue a PUT request to url (either a full URL or API
369+ shorthand) with the data.
370+
371+ """
372+ res = self .request (self ._resolve_url (url ), method = 'PUT' , data = data ,
373+ send_json = (self .api_path in JSON_PATHS ))
374+ return json .loads (res .read ())
307375
308376 def post (self , url , data = None ):
309- """"""
310- res = self .request (self ._resolve_url (url ), method = 'POST' , data = data )
377+ """Issue a POST request to url (either a full URL or API
378+ shorthand) with the data.
379+
380+ """
381+ res = self .request (self ._resolve_url (url ), method = 'POST' , data = data ,
382+ send_json = (self .api_path in JSON_PATHS ))
311383 return json .loads (res .read ())
312384
313385 def delete (self , url ):
314- """"""
386+ """Issue a DELETE request to the URL or API shorthand. """
315387 res = self .request (self ._resolve_url (url ), method = 'DELETE' )
316388 # Catch no content responses from some delete actions.
317389 if res .code == 204 :
318390 return json .loads ('[]' )
319391 return json .loads (res .read ())
320392
321393 def upload_creative (self , account_id , file_path ):
322- """"""
394+ """Upload a media creative to the account with ID
395+ account_id from the local file_path.
396+
397+ """
323398 # Thanks to nosklo for his answer on SO:
324399 # http://stackoverflow.com/a/681182
325400 boundary = '-----------------------------' + str (int (random .random ()* 1e10 ))
@@ -347,7 +422,13 @@ def upload_creative(self, account_id, file_path):
347422 # TODO: refactor Client.request.
348423 # TODO: Catch errors in attempt to upload.
349424 headers = {'content-type' : 'multipart/form-data; boundary=' + boundary }
350- url = self ._resolve_url ('/a/creative/uploadcreative' )
425+ if self .api_path == API_PATH_V1 :
426+ url = self ._resolve_url ('/a/creative/uploadcreative' )
427+ elif self .api_path == API_PATH_V2 :
428+ url = self ._resolve_url ('/creative/uploadcreative' )
429+ else :
430+ raise UnknownAPIFormatError (
431+ 'Unrecognized API path: %s' % self .api_path )
351432 req = urllib2 .Request (url , headers = headers , data = body )
352433 res = urllib2 .urlopen (req )
353434
0 commit comments