Skip to content

Instantly share code, notes, and snippets.

@jasonwaters
Created August 15, 2014 14:52
Show Gist options
  • Save jasonwaters/f9e22ba9bd56d31d87e7 to your computer and use it in GitHub Desktop.
Save jasonwaters/f9e22ba9bd56d31d87e7 to your computer and use it in GitHub Desktop.
# Copyright (c) 2010 AtTask, Inc.
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
# documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
# permit persons to whom the Software is furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or substantial portions of the
# Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
# COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
# OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
import urllib2
import json
import socket
from urllib import quote_plus
class StreamClient(object):
GET = 'GET'
POST = 'POST'
PUT = 'PUT'
DELETE = 'DELETE'
PATH_LOGIN = "/login"
PATH_LOGOUT = "/logout"
def __init__(self, url, username, password, apiKey=None):
"""
url -- the full url to the attask api
(http://yourdomain.attask-ondemand.com:8080/attask/api)
"""
self.url = url if not url.endswith('/') else url[:-1]
self.username = username
self.password = password
self.apiKey = apiKey
self.handle = None
self.session_id = None
self.user_id = None
self.timeout = socket._GLOBAL_DEFAULT_TIMEOUT #timeout in seconds for blocking operations like the connection attempt
def login(self):
"""
logs into attask
"""
params = {'username': self.username, 'password': self.password}
data = self.request(StreamClient.PATH_LOGIN, params, StreamClient.GET, logging_in=True)
self.session_id = data['sessionID']
self.user_id = data['userID']
def logout(self):
"logs out of attask"
self.request(StreamClient.PATH_LOGOUT, None, StreamClient.GET)
self.session_id = self.user_id = None
def get_list(self, objcode, ids, fields=None):
"""
Returns each object by id, similar to calling get for each id individually
objcode -- object type ie. ObjCode.PROJECT
ids -- list of ids to lookup
[fields] -- list of field names to return for each object
"""
path = '/%s' % objcode
return self.request(path, {'ids': ','.join(ids)}, fields)
def put(self, objcode, objid, params, fields=None):
"""
Updates an existing object, returns the updated object
objcode -- object type ie. ObjCode.PROJECT
objid -- id of object to update
params -- fields to update
[fields] -- list of field names to return for the object
"""
path = '/%s/%s' % (objcode, objid)
return self.request(path, params, StreamClient.PUT, fields)
def post(self, objcode, params, fields=None):
"""
Creates a new object, returns the new object
objcode -- object type ie. ObjCode.PROJECT
params -- values for object fields
[fields] -- list of field names to return for the object
"""
path = '/%s' % objcode
return self.request(path, params, StreamClient.POST, fields)
def get(self, objcode, objid, fields=None):
"""
Lookup an object by id
objcode -- object type ie. ObjCode.PROJECT
objid -- id to lookup
[fields] -- list of field names to return for each object
"""
path = '/%s/%s' % (objcode, objid)
return self.request(path, None, StreamClient.GET, fields)
def delete(self, objcode, objid, force=False):
"""
Deletes object with id objid
objcode -- object type ie. ObjCode.PROJECT
objid -- id of object to delete
[force=False] -- force delete objects with relationships,
ie. projects with task
"""
path = '/%s/%s' % (objcode, objid)
return self.request(path, {'force': force}, StreamClient.DELETE)
def search(self, objcode, params, fields=None):
"""
Search for objects
objcode -- object type ie. ObjCode.PROJECT
params -- name value keys to search for
[fields] -- fields to return for each search result
"""
path = '/%s/%s' % (objcode, 'search')
return self.request(path, params, StreamClient.GET, fields)
def request(self, path, params, method, fields=None, raw=False, timeout=None, logging_in=False, retry=0):
"""
Basic api request
path -- api url to open
params -- parameters for request
method -- a request method, StreamClient.GET,POST,PUT,DELETE
[fields] -- added to params as fields to return for request
[raw=False] -- returns the full json object, otherwise returns
the contents of the data json data field
"""
if not self.apiKey and not logging_in and not self.session_id:
self.login()
if not params:
params = {}
if self.apiKey is not None:
params['apiKey'] = self.apiKey
else:
params['sessionID'] = self.session_id
params['method'] = method
if fields:
params['fields'] = ','.join(fields)
encodedParams = ''
for key in params:
value = params[key]
if list == type(value):
for item in value:
encodedParams += self.encode(key, item)
else:
encodedParams += self.encode(key, value)
dest = self.url + path
timeout = timeout or self.timeout
try:
response = urllib2.urlopen(dest, encodedParams, timeout)
except urllib2.URLError, e:
if retry > 1:
#we tried to recover twice but are still failing. Bubble up the exception
raise StreamAPIException(e)
elif "Unauthorized" in e.reason: #we get this if the session expires, or incorrect password.
self.session_id = None #clear the session that is now invalid
return self.request(path, params, method, fields, raw, timeout, logging_in,
retry=retry + 1) #try the request again if the session expired (forcing a log-in)
else:
raise StreamAPIException(e)
data = json.load(response)
return data if raw else data['data']
def encode(self, key, value):
val = value
if str == type(value):
val = quote_plus(value)
return "&%s=%s" % (quote_plus(key), val if val is not None else '')
class StreamAPIException(Exception):
"Raised when a request fails"
class StreamNotModifiedException(Exception):
"Raised when saving an object that has not been modified"
class StreamClientNotSet(Exception):
"""Raised when calling an api method on an object without an
attached StreamClient object
"""
# CRUD wrapper for basic modifications
class AtTaskObject(object):
def __init__(self, data, streamclient=None):
self.__dict__['streamclient'] = streamclient
self.__dict__['data'] = data
self.__dict__['_dirty_fields'] = {}
def __getattr__(self, item):
return self.__dict__['data'][item]
def __setattr__(self, key, value):
self._dirty_fields[key] = True
self.data[key] = value
def __str__(self):
return json.dumps(self.data, indent=4)
def is_modified(self):
"Determines if object has been modified after creation"
return bool(len(m))
def save(self):
"""
Persists changes to streamclient instance
raises -- StreamClientNotSet if stream client was not passed in constructor
-- StreamNotModifiedException if no fields have changed
-- StreamAPIException if api call fails
"""
if not self.streamclient:
raise StreamClientNotSet()
params = dict([(key, self.data[key])
for key, val in self._dirty_fields.iteritems() if val])
if not len(params):
raise StreamNotModifiedException("No fields were modified.")
if self.data.has_key('ID'):
self.__dict__['data'] = self.streamclient.put(self.objCode, self.ID, params, self.data.keys())
else:
self.__dict__['data'] = self.streamclient.post(self.objCode, params, self.data.keys())
self.__dict__['_dirty_fields'] = {}
def delete(self, force=False):
"""
Deletes the current object by id
raises -- StreamClientNotSet if stream client was not passed in constructor
"""
if not self.streamclient:
raise StreamClientNotSet()
return self.streamclient.delete(self.objCode, self.ID, force)
# Supported object codes
class ObjCode:
PROJECT = 'proj'
TASK = 'task'
ISSUE = 'optask'
TEAM = 'team'
HOUR = 'hour'
TIMESHEET = 'tshet'
USER = 'user'
ASSIGNMENT = 'assgn'
USER_PREF = 'userpf'
CATEGORY = 'ctgy'
CATEGORY_PARAMETER = 'ctgypa'
PARAMETER = 'param'
PARAMETER_GROUP = 'pgrp'
PARAMETER_OPTION = 'popt'
PARAMETER_VALUE = 'pval'
ROLE = 'role'
GROUP = 'group'
NOTE = 'note'
DOCUMENT = 'docu'
DOCUMENT_VERSION = 'docv'
EXPENSE = 'expns'
CUSTOM_ENUM = 'cstem'
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment