Skip to content

Oauth2 Branch from original VersionOne.SDK.Python #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@ as the authors see fit.

## Overview


### Authenticating via Oauth2

We recommend using Oauth 2.0 to authenticate with the VersionOne instance. This prevents your client
from storing user credentials, and allows the user to manage permissions for multiple registered client applications.

This library uses Google's oauth2 library and its secrets and credentials file formats.
You must use another tool that writes these files before this library can be used to gain access to a VersionOne instance.
Once the files exist and are valid, authentication is automatic based on the stored secrets and credentials.

Our brief Oauth2 example program can be used to write that file. See https://github.com/versionone/versionone-oauth2-examples/tree/master/python27

### Dynamic reflection of all V1 asset types:

Just instantiate a V1Meta. All asset types defined on the server are available
Expand All @@ -24,13 +36,11 @@ as the authors see fit.

from v1pysdk import V1Meta

v1 = V1Meta() # Assumes localhost/VersionOne.Web, credentials Admin/Admin
v1 = V1Meta() # Assumes localhost/VersionOne.Web

v1 = V1Meta(
address = 'v1server.mycompany.com',
instance = 'VersionOne',
username = 'jsmith',
password = 'swordfish'
instance = 'VersionOne'
)

Story = v1.Story
Expand Down Expand Up @@ -198,7 +208,7 @@ as the authors see fit.
GOTCHA: All "required" attributes must be set, or the server will reject the data.

from v1pysdk import V1Meta
v1 = V1Meta(username='admin', password='admin')
v1 = V1Meta()
new_story = v1.Story.create(
Name = 'New Story',
Scope = v1.Scope.where(Name='2012 Projects').first()
Expand Down
3 changes: 2 additions & 1 deletion v1pysdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
class, "V1Meta", which exposes the types and operations found in a specified
VersionOne server (defaulting to localhost/VersionOne.Web).
"""

import logging
logging.basicConfig(level=logging.DEBUG)

from v1meta import V1Meta
from v1poll import V1Poll
Expand Down
106 changes: 52 additions & 54 deletions v1pysdk/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from urllib2 import Request, urlopen, HTTPError, HTTPBasicAuthHandler
from urllib import urlencode
from urlparse import urlunparse
import httplib2

try:
from xml.etree import ElementTree
Expand All @@ -12,46 +13,53 @@
from elementtree import ElementTree
from elementtree.ElementTree import Element

AUTH_HANDLERS = [HTTPBasicAuthHandler]

try:
from ntlm.HTTPNtlmAuthHandler import HTTPNtlmAuthHandler
AUTH_HANDLERS.append(HTTPNtlmAuthHandler)
except ImportError:
logging.warn("Windows integrated authentication module (ntlm) not found.")

import oauth2client
import oauth2client.clientsecrets
from oauth2client.file import Storage
from oauth2client.client import OAuth2WebServerFlow
from oauth2client.client import flow_from_clientsecrets


class V1Error(Exception): pass

class V1AssetNotFoundError(V1Error): pass

class V1OAuth2Error(V1Error): pass

class V1Oauth2CredentialsError(V1OAuth2Error): pass

class V1Oauth2ClientSecretsError(V1OAuth2Error): pass


class V1Server(object):
"Accesses a V1 HTTP server as a client of the XML API protocol"
API_PATH="/rest-1.oauth.v1"

def __init__(self, address='localhost', instance='VersionOne.Web', username='', password=''):
def __init__(self, address='localhost', instance='VersionOne.Web', client_secrets_file="client_secrets.json", stored_credentials_file="stored_credentials.json"):
self.address = address
self.instance = instance
self.username = username
self.password = password
self._install_opener()

def _install_opener(self):
base_url = self.build_url('')
password_manager = urllib2.HTTPPasswordMgrWithDefaultRealm()
password_manager.add_password(None, base_url, self.username, self.password)
handlers = [HandlerClass(password_manager) for HandlerClass in AUTH_HANDLERS]
self.opener = urllib2.build_opener(*handlers)
self.creds_storage = Storage(stored_credentials_file)
try:
self.flow = flow_from_clientsecrets(client_secrets_file,
scope='apiv1',
redirect_uri='urn:ietf:wg:oauth:2.0:oob'
)
except oauth2client.clientsecrets.InvalidClientSecretsError:
raise V1Oauth2ClientSecretsError("Stored client secrets file not found. Please use the command line tool to obtain it. For more information see http://docs.versionone.com/oauth2/something")
self.httpclient = httplib2.Http()
credentials = self.creds_storage.get()
if not credentials:
raise V1Oauth2CredentialsError("Stored client credentials not found. Please use the command line tool to obtain them. For more information see http://docs.versionone.com/oauth2/something")
credentials.authorize(self.httpclient)
logging.debug("Client has been authorized.")
logging.debug(credentials)
logging.debug(self.flow)

def http_get(self, url):
request = Request(url)
response = self.opener.open(request)
return response
return self.httpclient.request(url, "GET")

def http_post(self, url, data=''):
request = Request(url, data)
response = self.opener.open(request)
return response
return self.httpclient.request(url, 'POST', body=data)

def build_url(self, path, query='', fragment='', params='', port=80):
"So we dont have to interpolate urls ad-hoc"
Expand All @@ -64,40 +72,30 @@ def build_url(self, path, query='', fragment='', params='', port=80):
def fetch(self, path, query='', postdata=None):
"Perform an HTTP GET or POST depending on whether postdata is present"
url = self.build_url(path, query=query)
try:
if postdata is not None:
if isinstance(postdata, dict):
postdata = urlencode(postdata)
response = self.http_post(url, postdata)
else:
response = self.http_get(url)
body = response.read()
return (None, body)
except HTTPError, e:
if e.code == 401:
raise
body = e.fp.read()
return (e, body)
logging.debug(url)
if postdata is not None:
if isinstance(postdata, dict):
postdata = urlencode(postdata)
return self.http_post(url, postdata)
return self.http_get(url)

def get_xml(self, path, query='', postdata=None):
exception, body = self.fetch(path, query=query, postdata=postdata)
response, body = self.fetch(path, query=query, postdata=postdata)
document = ElementTree.fromstring(body)
if exception:
exception.xmldoc = document
if exception.code == 404:
raise V1AssetNotFoundError(exception)
elif exception.code == 400:
raise V1Error('\n'+body)
else:
raise V1Error(exception)
if response.status == 404:
raise V1AssetNotFoundError(response.reason)
elif response.status == 400:
raise V1Error('\n'+body)
elif response.status >= 400:
raise V1Error(response)
return document

def get_asset_xml(self, asset_type_name, oid):
path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid)
path = self.API_PATH + '/Data/{0}/{1}'.format(asset_type_name, oid)
return self.get_xml(path)

def get_query_xml(self, asset_type_name, where=None, sel=None):
path = '/rest-1.v1/Data/{0}'.format(asset_type_name)
path = self.API_PATH + '/Data/{0}'.format(asset_type_name)
query = {}
if where is not None:
query['Where'] = where
Expand All @@ -110,25 +108,25 @@ def get_meta_xml(self, asset_type_name):
return self.get_xml(path)

def execute_operation(self, asset_type_name, oid, opname):
path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid)
path = self.API_PATH + '/Data/{0}/{1}'.format(asset_type_name, oid)
query = {'op': opname}
return self.get_xml(path, query=query, postdata={})

def get_attr(self, asset_type_name, oid, attrname):
path = '/rest-1.v1/Data/{0}/{1}/{2}'.format(asset_type_name, oid, attrname)
path = self.API_PATH + '/Data/{0}/{1}/{2}'.format(asset_type_name, oid, attrname)
return self.get_xml(path)

def create_asset(self, asset_type_name, xmldata, context_oid=''):
body = ElementTree.tostring(xmldata, encoding="utf-8")
query = {}
if context_oid:
query = {'ctx': context_oid}
path = '/rest-1.v1/Data/{0}'.format(asset_type_name)
path = self.API_PATH + '/Data/{0}'.format(asset_type_name)
return self.get_xml(path, query=query, postdata=body)

def update_asset(self, asset_type_name, oid, update_doc):
newdata = ElementTree.tostring(update_doc, encoding='utf-8')
path = '/rest-1.v1/Data/{0}/{1}'.format(asset_type_name, oid)
path = self.API_PATH + '/Data/{0}/{1}'.format(asset_type_name, oid)
return self.get_xml(path, postdata=newdata)


Expand Down
2 changes: 1 addition & 1 deletion v1pysdk/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ def get_where_string(self):

def run_single_query(self, url_params={}, api="Data"):
urlquery = urlencode(url_params)
urlpath = '/rest-1.v1/{1}/{0}'.format(self.asset_class._v1_asset_type_name, api)
urlpath = self.asset_class._v1_v1meta.server.API_PATH + '/{1}/{0}'.format(self.asset_class._v1_asset_type_name, api)
# warning: tight coupling ahead
xml = self.asset_class._v1_v1meta.server.get_xml(urlpath, query=urlquery)
return xml
Expand Down
6 changes: 4 additions & 2 deletions v1pysdk/v1meta.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@


try:
from xml.etree import ElementTree
except ImportError:
Expand All @@ -10,8 +12,8 @@


class V1Meta(object):
def __init__(self, address='localhost', instance='VersionOne.Web', username='admin', password='admin'):
self.server = V1Server(address, instance, username, password)
def __init__(self, address='localhost', instance='VersionOne.Web'):
self.server = V1Server(address, instance)
self.global_cache = {}
self.dirtylist = []

Expand Down