Skip to content

Commit e679d70

Browse files
committed
better workspace and project scoping behavior, fixed inappropriate message on request timeout
1 parent 8ccf7cc commit e679d70

File tree

6 files changed

+83
-52
lines changed

6 files changed

+83
-52
lines changed

MANIFEST

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
1+
LICENSE
12
README.short
23
README.rst
3-
LICENSE
4-
MANIFEST
54
setup.py
65
template.cfg
76
rallyfire.py
@@ -15,6 +14,6 @@ pyral/restapi.py
1514
examples/getitem.py
1615
examples/periscope.py
1716
examples/showdefects.py
17+
examples/statecounts.py
1818
examples/crtask.py
1919
examples/uptask.py
20-
examples/statecounts.py

dists/pyral-0.8.11.tar.gz

108 KB
Binary file not shown.

dists/pyral-0.8.11.zip

127 KB
Binary file not shown.

pyral/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
__version__ = (0, 8, 11)
22
from .config import rallySettings
3-
from .restapi import Rally, RallyRESTAPIError
3+
from .restapi import Rally, RallyRESTAPIError, RallyUrlBuilder
44
from .rallyresp import RallyRESTResponse
55

pyral/context.py

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
REQUEST_TIME_LIMIT = 5 # in seconds
3030

3131
IPV4_ADDRESS_PATT = re.compile(r'^\d+\.\d+\.\d+\.\d+$')
32+
FORMATTED_ID_PATT = re.compile(r'^[A-Z]{1,2}\d+$')
3233

3334
##################################################################################################
3435

@@ -72,7 +73,7 @@ def serviceURL(self):
7273
def identity(self):
7374
workspace = self.workspace or 'None'
7475
project = self.project or 'None'
75-
return " | ".join([self.server, self.user, self.password, workspace, project])
76+
return " | ".join([self.server, self.user, self.password, workspace or "None", project or "None"])
7677

7778
def __repr__(self):
7879
return self.identity()
@@ -161,7 +162,7 @@ def check(self, server):
161162
if response.status_code == 404:
162163
if elapsed >= float(REQUEST_TIME_LIMIT):
163164
problem = "Request timed out on attempt to reach %s" % server
164-
if response.errors and 'NoneType' in response.errors[0]:
165+
elif response.errors and 'NoneType' in response.errors[0]:
165166
problem = "Target Rally host: '%s' non-existent or unreachable" % server
166167
else:
167168
#sys.stderr.write("404 Response for request\n")
@@ -184,6 +185,7 @@ def check(self, server):
184185
## print " RallyContextHelper.check got the User info ..."
185186
## sys.stdout.flush()
186187
##
188+
self.user_oid = response.next().oid
187189
self._loadSubscription()
188190
self._getDefaults(response)
189191
self._getWorkspacesAndProjects(workspace=self._defaultWorkspace, project=self._defaultProject)
@@ -259,9 +261,15 @@ def _getDefaults(self, response):
259261
if not self._projects:
260262
self._projects = {self._defaultWorkspace : [self._defaultProject]}
261263
if not self._workspace_ref:
262-
self._workspace_ref = {self._defaultWorkspace : wkspace_ref}
264+
if wkspace_ref.endswith('.js'):
265+
wkspace_ref = wkspace_ref[:-3]
266+
short_ref = "/".join(wkspace_ref.split('/')[-2:]) # we only need the 'workspace/<oid>' part to be a valid ref
267+
self._workspace_ref = {self._defaultWorkspace : short_ref}
263268
if not self._project_ref:
264-
self._project_ref = {self._defaultWorkspace : {self._defaultProject : proj_ref}}
269+
if proj_ref.endswith('.js'):
270+
proj_ref = proj_ref[:-3]
271+
short_ref = "/".join(proj_ref.split('/')[-2:]) # we only need the 'project/<oid>' part to be a valid ref
272+
self._project_ref = {self._defaultWorkspace : {self._defaultProject : short_ref}}
265273
self.defaultContext = RallyContext(self.server,
266274
self.user,
267275
self.password,
@@ -493,7 +501,11 @@ def identifyContext(self, **kwargs):
493501
self.context.workspace = workspace
494502

495503
project = None
496-
if 'project' in kwargs and kwargs['project']:
504+
if 'project' in kwargs:
505+
if not kwargs['project']:
506+
self.context.project = None
507+
return self.context, augments
508+
497509
project = kwargs['project']
498510
wks = workspace or self._currentWorkspace or self._defaultWorkspace
499511
if project not in self._projects[wks]:
@@ -555,8 +567,9 @@ def _getWorkspacesAndProjects(self, **kwargs):
555567
if workspace.Name not in self._workspaces:
556568
self._workspaces.append(workspace.Name)
557569
#self._workspace_ref[workspace.Name] = workspace._ref
558-
# we only need the workspace/123534 section to qualify as a valid ref
559-
self._workspace_ref[workspace.Name] = '/'.join(workspace._ref[:-3].split('/')[-2:])
570+
# we only need the 'workspace/<oid>' fragment to qualify as a valid ref
571+
wksp_ref = workspace._ref[:-3] if workspace._ref.endswith('.js') else workspace._ref
572+
self._workspace_ref[workspace.Name] = '/'.join(wksp_ref.split('/')[-2:])
560573
if workspace.Name not in self._projects:
561574
self._projects[ workspace.Name] = []
562575
self._project_ref[workspace.Name] = {}

pyral/restapi.py

Lines changed: 60 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
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

@@ -17,7 +18,6 @@
1718
import time
1819
import urllib
1920
import json
20-
from operator import itemgetter
2121

2222
import 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
108110
from .rallyresp import RallyRESTResponse, ErrorResponse
109111
from .hydrate import EntityHydrator
110112
from .context import RallyContext, RallyContextHelper
111113

112-
__all__ = ["Rally", "getResourceByOID", "hydrateAnInstance"]
114+
__all__ = ["Rally", "getResourceByOID", "hydrateAnInstance", "RallyUrlBuilder"]
113115

114116

115117
def _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

133134
class 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

Comments
 (0)