44#
55# pyral.restapi - Python Rally REST API module
66# round 8 version with GET, PUT, POST and DELETE operations, support multiple instances
7- # dependencies:
7+ # notable dependencies:
88# requests v0.8.2 or better
9+ # requests v0.9.3 recommended (0.10.x no longer works for Python 2.5)
910#
1011###################################################################################################
1112
1718import time
1819import urllib
1920import json
20- from operator import itemgetter
2121
2222import requests
2323
@@ -97,19 +97,21 @@ def getResourceByOID(context, entity, oid, **kwargs):
9797## print ""
9898## print " apparently no key to match: -->%s<--" % context
9999## print " context is a %s" % type(context)
100+ ##
100101 rally = rallyContext .get ('rally' )
101102 resp = rally ._getResourceByOID (context , entity , oid , ** kwargs )
102103 if 'unwrap' not in kwargs or not kwargs .get ('unwrap' , False ):
103104 return resp
104105 response = RallyRESTResponse (rally .session , context , "%s.x" % entity , resp , "full" , 1 )
105106 return response
106107
108+
107109# these imports have to take place after the prior class and function defs
108110from .rallyresp import RallyRESTResponse , ErrorResponse
109111from .hydrate import EntityHydrator
110112from .context import RallyContext , RallyContextHelper
111113
112- __all__ = ["Rally" , "getResourceByOID" , "hydrateAnInstance" ]
114+ __all__ = ["Rally" , "getResourceByOID" , "hydrateAnInstance" , "RallyUrlBuilder" ]
113115
114116
115117def _createShellInstance (context , entity_name , item_name , item_ref ):
@@ -127,7 +129,6 @@ def _createShellInstance(context, entity_name, item_name, item_ref):
127129 hydrator = EntityHydrator (context , hydration = "shell" )
128130 return hydrator .hydrateInstance (item )
129131
130-
131132##################################################################################################
132133
133134class Rally (object ):
@@ -147,8 +148,6 @@ def __init__(self, server=SERVER, user=USER_NAME, password=PASSWORD,
147148 self .version = version
148149 self ._inflated = False
149150 self .service_url = "%s://%s/%s" % (PROTOCOL , self .server , WEB_SERVICE % self .version )
150- self ._use_workspace_default = True
151- self ._use_project_default = True
152151 self .hydration = "full"
153152 self ._log = False
154153 self ._logDest = None
@@ -188,20 +187,16 @@ def __init__(self, server=SERVER, user=USER_NAME, password=PASSWORD,
188187 and kwargs ['workspace' ] != 'default' :
189188 if self .contextHelper .isAccessibleWorkspaceName (kwargs ['workspace' ]):
190189 self .contextHelper .setWorkspace (kwargs ['workspace' ])
191- self ._use_workspace_default = False
192190 __adjust_cache = True
193191 else :
194192 warning ("WARNING: Unable to use your workspace specification, that value is not listed in your subscription\n " )
195193
196194 if 'project' in kwargs and kwargs ['project' ] != self .contextHelper .currentContext ().project \
197195 and kwargs ['project' ] != 'default' :
198196 accessibleProjects = [name for name , ref in self .contextHelper .getAccessibleProjects (workspace = 'current' )]
199- ##
200- ## print "accessible projects: %s" % ", ".join(accessibleProjects)
201- ##
197+
202198 if kwargs ['project' ] in accessibleProjects :
203199 self .contextHelper .setProject (kwargs ['project' ])
204- self ._use_project_default = False
205200 __adjust_cache = True
206201 else :
207202 issue = ("Unable to use your project specification of '%s', "
@@ -232,9 +227,7 @@ def __init__(self, server=SERVER, user=USER_NAME, password=PASSWORD,
232227
233228 if __adjust_cache :
234229 _rallyCache [self .contextHelper .currentContext ()] = {'rally' : self }
235- ##
236- ## print "successfully intitialized a new Rally ifc ..."
237- ##
230+
238231
239232 def _wpCacheStatus (self ):
240233 """
@@ -368,7 +361,6 @@ def getProject(self):
368361 proj_name , proj_ref = self .contextHelper .getProject ()
369362 return _createShellInstance (context , 'Project' , proj_name , proj_ref )
370363
371-
372364 def getProjects (self , workspace = None ):
373365 """
374366 Return a list of minimally hydrated Project instances
@@ -503,16 +495,13 @@ def _getResourceByOID(self, context, entity, oid, **kwargs):
503495## print "_getResourceByOID, current contextDict: %s" % repr(contextDict)
504496## sys.stdout.flush()
505497##
506-
507498 context , augments = self .contextHelper .identifyContext (** contextDict )
508499 if augments :
509500 resource += ("?" + "&" .join (augments ))
510501##
511502## print "_getResourceByOID, modified contextDict: %s" % repr(context.asDict())
512503## sys.stdout.flush()
513504##
514-
515-
516505 full_resource_url = "%s/%s" % (self .service_url , resource )
517506 if self ._logAttrGet :
518507 self ._logDest .write ('%s GET %s\n ' % (timestamp (), resource ))
@@ -631,11 +620,17 @@ def get(self, entity, fetch=False, query=None, order=None, **kwargs):
631620 context , augments = self .contextHelper .identifyContext (** kwargs )
632621 workspace_ref = self .contextHelper .currentWorkspaceRef ()
633622 project_ref = self .contextHelper .currentProjectRef ()
634- if workspace_ref :
635- resource .augmentWorkspace (augments , workspace_ref , self ._use_workspace_default )
636- if project_ref :
637- resource .augmentProject (augments , project_ref , self ._use_project_default )
638- resource .augmentScoping (augments )
623+ ##
624+ ## print " workspace_ref: %s" % workspace_ref
625+ ## print " project_ref: %s" % project_ref
626+ ##
627+ if workspace_ref : # TODO: would we ever _not_ have a workspace_ref?
628+ if 'workspace' not in kwargs or ('workspace' in kwargs and kwargs ['workspace' ] is not None ):
629+ resource .augmentWorkspace (augments , workspace_ref )
630+ if project_ref :
631+ if 'project' not in kwargs or ('project' in kwargs and kwargs ['project' ] is not None ):
632+ resource .augmentProject (augments , project_ref )
633+ resource .augmentScoping (augments )
639634 resource = resource .build () # can also use resource = resource.build(pretty=True)
640635 full_resource_url = "%s/%s" % (self .service_url , resource )
641636
@@ -699,15 +694,23 @@ def get(self, entity, fetch=False, query=None, order=None, **kwargs):
699694 find = get # offer interface approximately matching Ruby Rally REST API, App SDK Javascript RallyDataSource
700695
701696
702- def put (self , entityName , itemData , workspace = None , project = None , ** kwargs ):
697+ def put (self , entityName , itemData , workspace = 'current' , project = 'current' , ** kwargs ):
703698 """
704- Return the newly created target entity item
699+ Given a Rally entityName, a dict with data that the newly created entity should contain,
700+ issue the REST call and return the newly created target entity item.
705701 """
702+ # see if we need to transform workspace / project values of 'current' to actual
703+ if workspace == 'current' :
704+ workspace = self .getWorkspace ().Name # just need the Name here
705+ if project == 'current' :
706+ project = self .getProject ().Name # just need the Name here
707+
706708 entityName = self ._officialRallyEntityName (entityName )
709+
707710 resource = "%s/create.js" % entityName .lower ()
708711 context , augments = self .contextHelper .identifyContext (workspace = workspace , project = project )
709712 if augments :
710- resource += ("& " + "&" .join (augments ))
713+ resource += ("? " + "&" .join (augments ))
711714 full_resource_url = "%s/%s" % (self .service_url , resource )
712715 item = {entityName : itemData }
713716 payload = json .dumps (item )
@@ -740,7 +743,17 @@ def put(self, entityName, itemData, workspace=None, project=None, **kwargs):
740743 create = put # a more intuitive alias for the operation
741744
742745
743- def post (self , entityName , itemData , workspace = None , project = None , ** kwargs ):
746+ def post (self , entityName , itemData , workspace = 'current' , project = 'current' , ** kwargs ):
747+ """
748+ Given a Rally entityName, a dict with data that the entity should be updated with,
749+ issue the REST call and return a representation of updated target entity item.
750+ """
751+ # see if we need to transform workspace / project values of 'current' to actual
752+ if workspace == 'current' :
753+ workspace = self .getWorkspace ().Name # just need the Name here
754+ if project == 'current' :
755+ project = self .getProject ().Name # just need the Name here
756+
744757 entityName = self ._officialRallyEntityName (entityName )
745758
746759 oid = itemData .get ('ObjectID' , None )
@@ -751,7 +764,7 @@ def post(self, entityName, itemData, workspace=None, project=None, **kwargs):
751764 fmtIdQuery = 'FormattedID = "%s"' % formattedID
752765 response = self .get (entityName , fetch = "ObjectID" , query = fmtIdQuery ,
753766 workspace = workspace , project = project )
754- if response .status_code != 200 :
767+ if response .status_code != 200 or response . resultCount == 0 :
755768 raise RallyRESTAPIError ('Target %s %s could not be located' % (entityName , formattedID ))
756769
757770 target = response .next ()
@@ -764,7 +777,7 @@ def post(self, entityName, itemData, workspace=None, project=None, **kwargs):
764777 resource = '%s/%s.js' % (entityName .lower (), oid )
765778 context , augments = self .contextHelper .identifyContext (workspace = workspace , project = project )
766779 if augments :
767- resource += ("& " + "&" .join (augments ))
780+ resource += ("? " + "&" .join (augments ))
768781 full_resource_url = "%s/%s" % (self .service_url , resource )
769782##
770783## print "resource: %s" % resource
@@ -787,8 +800,20 @@ def post(self, entityName, itemData, workspace=None, project=None, **kwargs):
787800 update = post # a more intuitive alias for the operation
788801
789802
790- def delete (self , entityName , itemIdent , workspace = None , project = None , ** kwargs ):
803+ def delete (self , entityName , itemIdent , workspace = 'current' , project = 'current' , ** kwargs ):
804+ """
805+ Given a Rally entityName, an identification of a specific Rally instnace of that
806+ entity (in either OID or FormattedID format), issue the REST DELETE call and
807+ return an indication of whether the delete operation was successful.
808+ """
809+ # see if we need to transform workspace / project values of 'current' to actual
810+ if workspace == 'current' :
811+ workspace = self .getWorkspace ().Name # just need the Name here
812+ if project == 'current' :
813+ project = self .getProject ()[0 ].Name # just need the Name here
814+
791815 entityName = self ._officialRallyEntityName (entityName )
816+
792817 # guess at whether itemIdent is an ObjectID or FormattedID via
793818 # regex matching (all digits or 1-2 upcase chars + digits)
794819 objectID = itemIdent # at first assume itemIdent is the ObjectID
@@ -820,7 +845,7 @@ def delete(self, entityName, itemIdent, workspace=None, project=None, **kwargs):
820845 self ._logDest .flush ()
821846##
822847## if kwargs.get('debug', False):
823- ## print response
848+ ## print response.status_code, response.headers, response.content
824849##
825850 errorResponse = ErrorResponse (response .status_code , response .content )
826851 response = RallyRESTResponse (self .session , context , resource , errorResponse , self .hydration , 0 )
@@ -847,7 +872,7 @@ def delete(self, entityName, itemIdent, workspace=None, project=None, **kwargs):
847872
848873 def allowedValueAlias (self , entity , refUrl ):
849874 """
850- use the _allowedValueAlias as a cache. A cache hit results from
875+ Use the _allowedValueAlias as a cache. A cache hit results from
851876 having an entity key in _allowedValueAlias AND and entry for the OID
852877 contained in the refUrl, the return is the OID and the alias value.
853878 If there is no cache hit for the entity, issue a GET against
@@ -987,7 +1012,6 @@ def build(self, pretty=None):
9871012 if self .pretty :
9881013 qualifiers .append ('pretty=true' )
9891014
990- #resource += ("&" + "&".join(qualifiers))
9911015 resource += "&" .join (qualifiers )
9921016 return resource
9931017
@@ -1029,20 +1053,15 @@ def _encode(condition):
10291053
10301054 return None
10311055
1032-
1033- def augmentWorkspace (self , augments , workspace_ref , use_default ):
1056+ def augmentWorkspace (self , augments , workspace_ref ):
10341057 wksp_augment = [aug for aug in augments if aug .startswith ('workspace=' )]
1035- if use_default :
1036- self .workspace = "workspace=%s" % workspace_ref
1058+ self .workspace = "workspace=%s" % workspace_ref
10371059 if wksp_augment :
10381060 self .workspace = wksp_augment [0 ]
10391061
1040-
1041- def augmentProject (self , augments , project_ref , use_default ):
1062+ def augmentProject (self , augments , project_ref ):
10421063 proj_augment = [aug for aug in augments if aug .startswith ('project=' )]
10431064 self .project = "project=%s" % project_ref
1044- if use_default :
1045- self .project = "project=%s" % project_ref
10461065 if proj_augment :
10471066 self .project = proj_augment [0 ]
10481067
0 commit comments