Skip to content
Draft
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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,21 @@ config['grantType'] = 'client_credentials'
# Since the grant type is client_credentials a
# ClientCredentialsConfiguration object is returned
try:

# To load configuration at runtime, pass the config dictionary argument to
# the ConnectionConfiguration:
ClientCredentialsConfiguration = ConnectionConfiguration().init(config)

# Note: The configuration can also be loaded from an INI config file. The
# default location for the config file is $HOME/adp_connection.ini. A
# custom location for the config file can be specified by setting the
# ADP_CONNECTION_CONFIG environment variable.

# To load a configuration object from a INI config file, initialize the
# ConnectionConfiguration without a config dictionary:

# ClientCredentialsConfiguration = ConnectionConfiguration().init()

# Using the new configuration object create a connection
ccConnection = ADPAPIConnectionFactory().createConnection(ClientCredentialsConfiguration)

Expand Down
1 change: 1 addition & 0 deletions adp_connection/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from __future__ import absolute_import
from adp_connection.version import __version__ # NOQA
18 changes: 10 additions & 8 deletions adp_connection/democlient/sampleApp.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from __future__ import absolute_import
from __future__ import print_function
import sys
from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from urlparse import urlparse, parse_qs
from six.moves.BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer
from six.moves.urllib.parse import urlparse, parse_qs
from os import curdir, sep
from adp_connection.lib import *
from adp_connection import __version__
Expand Down Expand Up @@ -96,7 +98,7 @@ def do_GET(self):
else:
resp = '<b>Not Connected!</b>'
except ConfigError as conferr:
print conferr.msg
print(conferr.msg)
except ConnectError as connecterr:
resp = '<b>Connection Error</b>'
resp = resp + '<br>Class: ' + connecterr.cname
Expand Down Expand Up @@ -161,10 +163,10 @@ def do_GET(self):
self.end_headers()
return
except ConfigError as conferr:
print conferr.msg
print(conferr.msg)
raise
except ConnectError as connecterr:
print connecterr.msg
print(connecterr.msg)
raise

# Handle the callback request after a login attempt for an
Expand Down Expand Up @@ -217,7 +219,7 @@ def do_GET(self):
resp = resp + '<br>Error Message: ' + connecterr.msg
resp = resp + '<br>API Class: ' + connecterr.cname
except:
print "Unexpected error:", str(sys.exc_info())
print("Unexpected error:", str(sys.exc_info()))
finally:
self.send_response(200)
self.send_header('Content-type', 'text/html')
Expand Down Expand Up @@ -262,11 +264,11 @@ def do_GET(self):
# Create a web server and define the handler to manage the
# incoming request
server = HTTPServer(('', PORT_NUMBER), httpHandler)
print 'adp-connection-python version ' + __version__ + ' started httpserver on port ', PORT_NUMBER
print('adp-connection-python version ' + __version__ + ' started httpserver on port ', PORT_NUMBER)

# Wait forever for incoming htto requests
server.serve_forever()

except KeyboardInterrupt:
print '^C received, shutting down the web server'
print('^C received, shutting down the web server')
server.socket.close()
1 change: 1 addition & 0 deletions adp_connection/docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
# All configuration values have a default; values that are commented out
# serve to show the default.

from __future__ import absolute_import
import sys
import os

Expand Down
9 changes: 9 additions & 0 deletions adp_connection/example_config.ini
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[ADP Connection]
clientID=<<CLIENT_ID>>
clientSecret=<<CLIENT_SECRET>>
sslCertPath=/path/to/certs/ssl.pem
sslKeyPath=/path/to/certs/auth.key
apiRequestURL=https://api.adp.com
tokenServerURL=https://accounts.adp.com/auth/oauth/v2/token
disconnectURL=https://accounts.adp.com/auth/oauth/v2/logout
grantType=client_credentials
9 changes: 5 additions & 4 deletions adp_connection/lib/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from connectionconfiguration import *
from adpapiconnection import *
from connectexceptions import *
from adpapiconnectionfactory import *
from __future__ import absolute_import
from .connectionconfiguration import *
from .adpapiconnection import *
from .connectexceptions import *
from .adpapiconnectionfactory import *
130 changes: 126 additions & 4 deletions adp_connection/lib/adpapiconnection.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from __future__ import absolute_import
import requests
import datetime
import uuid
import logging
from connectexceptions import *
from connectionconfiguration import *
from .connectexceptions import *
from .connectionconfiguration import *
from adp_connection import __version__


Expand All @@ -35,10 +36,11 @@ class ADPAPIConnection(object):
connectionConfiguration: instance of the ConnectionConfiguration class
that was used to instantiate the connection """

connection = {'status': 'availabe', 'type': 'unknown', 'token': '',
connection = {'status': 'available', 'type': 'unknown', 'token': '',
'expires': '', 'sessionState': ''}
connectionConfiguration = None
userAgent = 'adp-userinfo-python/' + __version__
lastEventId = None

def isConnectedIndicator(self):
""" Returns: a boolen depending on whether the connection
Expand Down Expand Up @@ -115,7 +117,127 @@ def disconnect(self):
headers=(headers))
logging.debug(r.status_code)
self.connection = {'status': 'ready', 'type': 'unknown', 'token': '',
'expires': '', 'state': ''}
'expires': '', 'sessionState': ''}

def reconnect(self, url, method, headers={}, params={}, data={}):
"""Reconnect to ADP API after token expiration."""
self.disconnect()
self.connect()
return self.request(url, method=method, headers=headers, params=params, data=data)

def request(self, url, method='get', headers={}, params={}, data={}):
"""Expose an HTTP Requests object configured to work with the ADP API connection.
Attempt an authenticated request to the ADP API. Pass access token and
TLS certificate to configure bearer token authentication for request. Connect to
ADP automatically. Attempt to reconnect when token expiration is detected.

Args:
url (str): The API url endpoint.
method (str): The HTTP method: 'get', 'post', or 'delete' are supported.
headers (dict): Additional http headers to supply with the request.
params (dict): Query string parameters for the request.
data (dict): POST variables for the request.

Returns:
The HTTP Requests object containing the http result object.
"""
if not self.isConnectedIndicator():
self.connect()

headers['Authorization'] = 'Bearer {}'.format(self.getAccessToken())
if 'roleCode' not in headers.keys():
headers['roleCode'] = 'practitioner'
cert = (
self.getConfig().getSSLCertPath(),
self.getConfig().getSSLKeyPath(),
)
requestKwargs = {
'headers': headers,
'verify': False,
'cert': cert,
}
if method == 'post' and data:
requestKwargs['data'] = data

if method in ['get', 'post'] and params:
requestKwargs['params'] = params

apiUrl = self.connectionConfiguration.getApiRequestURL()
requestUrl = '{}/{}'.format(apiUrl, url)

requestMethod = getattr(requests, method)
res = requestMethod(requestUrl, **requestKwargs)

# Attempt reconnect when response is 401 - Unauthorized and token is expired.
if res.status_code == 401 and self.getExpiration() <= datetime.datetime.now():
return self.reconnect(url, method, headers, params, data)

return res

def loadEvent(self, delete=False, longPoll=True):
""" Load the next event notification from the ADP API event notification
system. Notifications function as a first-in-first-out queue.

Keyword arguments:
delete -- Whether to delete the last notification after retrieval. If True,
each call to this method will retrieve a new event notification, since the
notifications are deleted upon retrieval. If False, a subsequent call to
ADPAPIConnection.deleteLastEvent() is needed to increment the queue and
return the next event.
longPoll -- Whether to use the HTTP long polling functionality, where the
request will hang for 15 seconds waiting for an event. If no event is
returned after this interval, the response is returned.

Returns:
A Requests object containing the http response.
"""
endpoint = 'core/v1/event-notification-messages'
headers = {}
messageIdKey = 'adp-msg-msgid'
if longPoll:
headers['prefer'] = '/adp/long-polling'

result = self.get(endpoint, headers=headers)

if result.status_code == 200:
messageId = result.headers[messageIdKey]
logging.debug('Event message ID: {}'.format(messageId))
self.lastEventId = messageId
if delete:
self.deleteLastEvent(eventId=messageId)
return result

def deleteLastEvent(self, eventId=None):
"""Delete the last event notification to provide the next one in the
queue. Use the supplied event ID, or check for a previously stored
event ID if one is not supplied."""
endpoint = 'core/v1/event-notification-messages/{}'
if eventId is None:
lastEventId = self.lastEventId
else:
lastEventId = eventId
if lastEventId is None:
raise ValueError("No event ID was provided.")

logging.debug('Deleting Event message ID: {}'.format(lastEventId))
deleteUrl = endpoint.format(lastEventId)
deleteResult = self.delete(deleteUrl)
if deleteResult.status_code == 200:
self.lastEventId = None
else:
logging.debug('Unable to delete event {}'.format(lastEventId))

def get(self, url, headers={}, params={}):
""" Convenience method for creating HTTP GET requests"""
return self.request(url, headers=headers, params=params)

def post(self, url, headers={}, params={}, data={}):
""" Convenience method for creating HTTP POST requests"""
return self.request(url, method='post', headers=headers, params=params, data=data)

def delete(self, url, headers={}):
""" Convenience method for creating HTTP DELETE requests"""
return self.request(url, method='delete', headers=headers)


class ClientCredentialsConnection(ADPAPIConnection):
Expand Down
3 changes: 2 additions & 1 deletion adp_connection/lib/adpapiconnectionfactory.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from adpapiconnection import *
from __future__ import absolute_import
from .adpapiconnection import *

logging.basicConfig(level=logging.DEBUG)

Expand Down
1 change: 1 addition & 0 deletions adp_connection/lib/connectexceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ class Error(Exception):
errDict['responseTypeBad'] = {'errCode': 'ConfErr-013', 'errMsg': 'incorrect responseType'}
errDict['scopeBad'] = {'errCode': 'ConfErr-013', 'errMsg': 'incorrect scope'}
errDict['initBad'] = {'errCode': 'ConfErr-014', 'errMsg': 'configuration not inited'}
errDict['configFileNotFound'] = {'errCode': 'ConfErr-015', 'errMsg': 'Unable to load configuration file from {}'}

def __init__(self, cname, code, msg):
self.cname = cname
Expand Down
48 changes: 46 additions & 2 deletions adp_connection/lib/connectionconfiguration.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,11 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

from __future__ import absolute_import
import logging
from connectexceptions import *
import os
import configparser
from .connectexceptions import *


class ConnectionConfiguration(object):
Expand All @@ -33,6 +36,8 @@ class ConnectionConfiguration(object):

config = dict({})
initDone = False
CONF_SECTION_KEY = 'ADP Connection'
CONF_FILENAME = 'adp_connection.ini'

def __init__(self):
""" Initialize the dictionary keys:
Expand Down Expand Up @@ -102,15 +107,21 @@ def getDisconnectURL(self):
def getGrantType(self):
return self.config['grantType']

def init(self, configObj):
def init(self, configObj=None):
""" Method to initialize the common config parameters:
clientID, clientSecret, sslCertPath, sslKeyPath,
tokenServerURL, apiRequestURL, disconnectURL and grantType.

Load and validate config from supplied dictionary. If no config is
present at runtime, check for a local config file.

Attributes:
configObj: dictionary containing the config values to be
initialized. """

if configObj is None:
configObj = self.attemptConfigFile()

logging.debug('Initializing Config Object')
if ('clientID' in configObj):
self.setClientID(configObj['clientID'])
Expand Down Expand Up @@ -165,6 +176,39 @@ def init(self, configObj):
logging.debug('Conf Error: ' + Error.errDict['grantTypeBad']['errCode'] + ': ' + Error.errDict['grantTypeBad']['errMsg'])
raise ConfigError(self.__class__.__name__, Error.errDict['grantTypeBad']['errCode'], Error.errDict['grantTypeBad']['errMsg'])

def attemptConfigFile(self):
"""Attempt to locate and parse an INI configuration file to configure ADP
client connection. Check for an ADP_CONNECTION_CONFIG environment variable
containing the filesystem location for config file. If no environment variable
is present, check for a configuration file in the user's home directory."""
homeDirectory = os.path.expanduser('~')
defaultName = self.CONF_FILENAME
defaultLocation = os.path.join(homeDirectory, defaultName)
confFileLocation = os.getenv('ADP_CONNECTION_CONFIG')

if confFileLocation is None:
confFileLocation = defaultLocation

if not os.path.exists(confFileLocation):
raise ConfigError(
self.__class__.__name__,
Error.errDict['configFileNotFound']['errCode'],
Error.errDict['configFileNotFound']['errMsg'].format(confFileLocation),
)

logging.debug('Loading config file: {}..'.format(confFileLocation))
confParser = configparser.ConfigParser()
confParser.read(confFileLocation)

config = dict()
for field in self.config.keys():
fileField = confParser.get(self.CONF_SECTION_KEY, field, fallback=None)
if fileField:
config[field] = fileField
else:
config[field] = self.config[field]
logging.debug('Successfully loaded config file.')
return config

class ClientCredentialsConfiguration(ConnectionConfiguration):
""" Client credentials sub class of the ConnectionConfiguration object """
Expand Down
2 changes: 1 addition & 1 deletion adp_connection/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
# express or implied. See the License for the specific language
# governing permissions and limitations under the License.

__version__ = '1.0.1' # NOQA
__version__ = '1.0.1.post1' # NOQA
Loading