Skip to content

Commit 07afb54

Browse files
committed
Merge pull request #2 from openx/v2
Added support for APIv2 style calls
2 parents a6b6b56 + 9136ec9 commit 07afb54

3 files changed

Lines changed: 112 additions & 24 deletions

File tree

History.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
0.4.0 / 2013-07-23
2+
==================
3+
* Added handling for API v2
4+
* Refined docstrings throughout code
5+
16
0.3.1 / 2013-06-04
27
==================
38
* Removed: Realm Support

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ httplib2. Instead it uses urllib2 as the HTTP transport.
66

77
It currently supports Python 2.4 - 2.7, with 3.x support comming in the future.
88

9+
As of version 0.4.0, ox3apiclient supports API v2. If your instance is v2,
10+
set the api_path option to "/ox/4.0".
11+
912
Basic usage:
1013

1114
````python
@@ -101,5 +104,4 @@ ox = ox3apiclient.Client(
101104
consumer_secret=consumer_secret)
102105

103106
ox.logon(email, password)
104-
````
105-
Test
107+
````

ox3apiclient/__init__.py

Lines changed: 103 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -33,23 +33,37 @@
3333

3434
import urlparse
3535

36-
__version__ = '0.3.1'
36+
__version__ = '0.4.0'
3737

3838
REQUEST_TOKEN_URL = 'https://sso.openx.com/api/index/initiate'
3939
ACCESS_TOKEN_URL = 'https://sso.openx.com/api/index/token'
4040
AUTHORIZATION_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

4451
class 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 += '\nLegal 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

Comments
 (0)