Skip to content

Commit e349717

Browse files
committed
Added support for loading config file. Closes #1
2 parents 6e88438 + 21b4397 commit e349717

6 files changed

Lines changed: 345 additions & 212 deletions

File tree

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
ox_*.py
1+
.ox3rc
22
*.pyc

ox3apiclient/__init__.py

Lines changed: 252 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,256 @@
11
# -*- coding: utf-8 -*-
22

3-
from ox3apiclient import *
3+
import ConfigParser
4+
import cookielib
5+
import json
6+
import oauth2 as oauth
7+
import urllib
8+
import urllib2
9+
import urlparse
410

511
__version__ = '0.1.0'
12+
13+
REQUEST_TOKEN_URL = 'https://sso.openx.com/api/index/initiate'
14+
ACCESS_TOKEN_URL = 'https://sso.openx.com/api/index/token'
15+
AUTHORIZATION_URL = 'https://sso.openx.com/login/process'
16+
API_PATH = '/ox/3.0'
17+
HTTP_METHOD_OVERRIDES = ['DELETE', 'PUT']
18+
19+
class Client(object):
20+
21+
def __init__(self, domain, realm, consumer_key, consumer_secret,
22+
callback_url='oob',
23+
scheme='http',
24+
request_token_url=REQUEST_TOKEN_URL,
25+
access_token_url=ACCESS_TOKEN_URL,
26+
authorization_url=AUTHORIZATION_URL,
27+
api_path=API_PATH):
28+
"""
29+
30+
domain -- Your UI domain. The API is accessed off this domain.
31+
realm -- Your sso realm. While not necessary for all OAuth
32+
implementations, it is a requirement for OpenX Enterprise
33+
consumer_key -- Your consumer key.
34+
consumer_secret -- Your consumer secret.
35+
callback_url -- Callback URL to redirect to on successful authorization.
36+
We default to 'oob' for headless login.
37+
request_token -- Only override for debugging.
38+
access_token -- Only override for debugging.
39+
authorization_url -- Only override for debugging.
40+
api_path -- Only override for debugging.
41+
"""
42+
self.domain = domain
43+
self.realm = realm
44+
self.consumer_key = consumer_key
45+
self.consumer_secret = consumer_secret
46+
self.callback_url = callback_url
47+
self.scheme=scheme
48+
self.request_token_url = request_token_url
49+
self.access_token_url = access_token_url
50+
self.authorization_url = authorization_url
51+
self.api_path = api_path
52+
53+
# You shouldn't need to access the oauth2 consumer and token objects
54+
# directly so we'll keep them "private".
55+
self._consumer = oauth.Consumer(self.consumer_key, self.consumer_secret)
56+
self._token = None
57+
58+
# Similarly you probably won't need to access the cookie jar directly,
59+
# so it is private as well.
60+
self._cookie_jar = cookielib.LWPCookieJar()
61+
opener = \
62+
urllib2.build_opener(urllib2.HTTPCookieProcessor(self._cookie_jar))
63+
64+
urllib2.install_opener(opener)
65+
66+
def _sign_request(self, req):
67+
"""Utility method to sign a request."""
68+
parameters = {'oauth_callback': self.callback_url}
69+
headers = req.headers
70+
data = req.data
71+
72+
# Add any (POST) data to the parameters to be signed in the OAuth
73+
# request.
74+
if data:
75+
parameters.update(data)
76+
77+
# Create a temporary oauth2 Request object and sign it so we can steal
78+
# the Authorization header.
79+
oauth_req = oauth.Request.from_consumer_and_token(
80+
consumer=self._consumer,
81+
token=self._token,
82+
http_method=req.get_method(),
83+
http_url=req.get_full_url(),
84+
parameters=parameters,
85+
is_form_encoded=True)
86+
87+
oauth_req.sign_request(
88+
oauth.SignatureMethod_HMAC_SHA1(),
89+
self._consumer,
90+
self._token)
91+
92+
# Update our original requests headers to include the OAuth Authorization
93+
# header and return it.
94+
req.headers.update(oauth_req.to_header(realm=self.realm))
95+
return \
96+
urllib2.Request(req.get_full_url(), headers=req.headers, data=data)
97+
98+
def request(self, url, method='GET', headers={}, data=None, sign=False):
99+
"""Helper method to make a (optionally OAuth signed) HTTP request."""
100+
101+
# Since we are using a urllib2.Request object we need to assign a value
102+
# other than None to "data" in order to make the request a POST request,
103+
# even if there is no data to post.
104+
if method == 'POST':
105+
data = data if data else ''
106+
107+
req = urllib2.Request(url, headers=headers, data=data)
108+
109+
# We need to set the request's get_method function to return a HTTP
110+
# method for any values other than GET or POST.
111+
if method in HTTP_METHOD_OVERRIDES:
112+
req.get_method = lambda: method
113+
114+
if sign:
115+
req = self._sign_request(req)
116+
117+
# Stringify data.
118+
if data:
119+
req.add_data(urllib.urlencode(req.get_data()))
120+
121+
return urllib2.urlopen(req)
122+
123+
def fetch_request_token(self):
124+
"""Helper method to fetch and set request token.
125+
126+
Returns token string.
127+
"""
128+
res = self.request(url=self.request_token_url, method='POST', sign=True)
129+
self._token = oauth.Token.from_string(res.read())
130+
return self._token
131+
132+
def authorize_token(self, email, password):
133+
"""Helper method to authorize."""
134+
data = {
135+
'email': email,
136+
'password': password,
137+
'oauth_token': self._token.key}
138+
139+
res = self.request(
140+
url=self.authorization_url,
141+
method='POST',
142+
data=data,
143+
sign=True)
144+
145+
verifier = urlparse.parse_qs(res.read())['oauth_verifier'][0]
146+
self._token.set_verifier(verifier)
147+
148+
def fetch_access_token(self):
149+
"""Helper method to fetch and set access token.
150+
151+
Returns token string.
152+
"""
153+
res = self.request(url=self.access_token_url, method='POST', sign=True)
154+
self._token = oauth.Token.from_string(res.read())
155+
return self._token
156+
157+
def validate_session(self):
158+
"""Validate an API session."""
159+
160+
# We need to store our access token as the openx3_access_token cookie.
161+
# This cookie will be passed to all future API requests.
162+
cookie = cookielib.Cookie(
163+
version=0,
164+
name='openx3_access_token',
165+
value=self._token.key,
166+
port=None,
167+
port_specified=False,
168+
domain=self.domain,
169+
domain_specified=True,
170+
domain_initial_dot=False,
171+
path='/',
172+
path_specified=True,
173+
secure=False,
174+
expires=None,
175+
discard=False,
176+
comment=None,
177+
comment_url=None,
178+
rest={})
179+
180+
self._cookie_jar.set_cookie(cookie)
181+
182+
url = '%s://%s%s/a/session/validate' % (self.scheme,
183+
self.domain,
184+
self.api_path)
185+
186+
res = self.request(url=url, method='PUT')
187+
return res.read()
188+
189+
def _resolve_url(self, url):
190+
""""""
191+
parse_res = urlparse.urlparse(url)
192+
if not parse_res.scheme:
193+
url ='%s://%s%s%s' % (self.scheme, self.domain, self.api_path,
194+
parse_res.path)
195+
url = url + '?' + parse_res.query if parse_res.query else url
196+
197+
return url
198+
199+
def get(self, url):
200+
""""""
201+
res = self.request(self._resolve_url(url), method='GET')
202+
return json.loads(res.read())
203+
204+
def post(self, url, data=None):
205+
""""""
206+
res = self.request(self._resolve_url(url), method='POST', data=data)
207+
return json.loads(res.read())
208+
209+
def delete(self, url):
210+
""""""
211+
res = self.request(self._resolve_url(url), method='DELETE')
212+
return json.loads(res.read())
213+
214+
215+
def client_from_file(file_path='.ox3rc', env=None):
216+
"""Return an instance of ox3apiclient.Client with data from file_path.
217+
218+
Keyword arguments:
219+
file_path -- the file to load. Default is '.ox3rc' form current dir.
220+
env -- the env section to load. Default will be first env section.
221+
222+
"""
223+
cp = ConfigParser.RawConfigParser()
224+
cp.read(file_path)
225+
226+
# Load default env if no env is specified. The default env is just the first
227+
# env listed.
228+
env_ids = [e for e in cp.get('ox3apiclient', 'envs').split('\n') if e]
229+
env = env if env else env_ids[0]
230+
231+
# Required parameters for a ox3apiclient.Client instance.
232+
required_params = [
233+
'domain',
234+
'realm',
235+
'consumer_key',
236+
'consumer_secret']
237+
238+
client_params = {}
239+
240+
# Load required parameters.
241+
try:
242+
for key in required_params:
243+
client_params[key] = cp.get(env, key)
244+
except ConfigParser.NoOptionError:
245+
err_msg = "Missing required option: '%s'" % key
246+
raise Exception(err_msg)
247+
248+
# TODO: Add support for optional parameters.
249+
250+
client = Client(
251+
domain=client_params['domain'],
252+
realm=client_params['realm'],
253+
consumer_key=client_params['consumer_key'],
254+
consumer_secret=client_params['consumer_secret'])
255+
256+
return client

0 commit comments

Comments
 (0)