Skip to content

Commit 3b257c9

Browse files
committed
context improvements, attachmennts convenience methods
1 parent 6991b22 commit 3b257c9

File tree

14 files changed

+432
-52
lines changed

14 files changed

+432
-52
lines changed

build_dist.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
import zipfile
1212

1313
PACKAGE_NAME = "pyral"
14-
VERSION = "0.8.12"
14+
VERSION = "0.9.1"
1515

1616
AUX_FILES = ['MANIFEST.in',
1717
'LICENSE',
@@ -25,7 +25,14 @@
2525
'periscope.py',
2626
'showdefects.py',
2727
'crtask.py',
28-
'uptask.py'
28+
'uptask.py',
29+
'statecounts.py',
30+
'repoitems.py',
31+
'typedefs.py',
32+
'wkspcounts.py',
33+
'builddefs.py',
34+
'creattach.py',
35+
'get_attachments.py',
2936
]
3037
DOC_FILES = ['doc/Makefile',
3138
'doc/source/conf.py',
@@ -47,9 +54,12 @@
4754
# The TEST_FILES are **NOT** placed into the distribution packages
4855
#
4956
TEST_FILES = ['test/test_conn.py',
50-
'test/test_query.py',
51-
'test/test_inflation.py',
57+
'test/test_context.py',
5258
'test/test_convenience.py',
59+
'test/test_inflation.py',
60+
'test/test_query.py',
61+
'test/test_wksprj_setting.py',
62+
'test/test_attachments.py',
5363
]
5464

5565
################################################################################

dists/pyral-0.9.1.tar.gz

-2 Bytes
Binary file not shown.

dists/pyral-0.9.1.zip

-1 Bytes
Binary file not shown.

doc/source/interface.rst

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -353,6 +353,40 @@ pyral.Rally instance convenience methods
353353
Given an entityName and and attributeName (assumed to be valid for the entityName)
354354
issue a request to obtain a list of allowed values for the attribute.
355355

356+
.. method:: addAttachment(artifact, filename, mime_type='text/plain')
357+
358+
Given an artifact (actual or FormattedID for an artifact), validate that
359+
it exists and then attempt to add an Attachment with the name and
360+
contents of filename into Rally and associate that Attachment with the
361+
Artifact.
362+
363+
.. method:: addAttachments(artifact, attachments)
364+
365+
Given an artifact (either actual or FormattedID) and a list of dicts with
366+
each dict having keys and values for name (or Name), mime_type (or MimeType) and
367+
content_type (or ContentType), add an Attachment corresponding to each dict in
368+
the attachments list and associate it with the referenced Artifact.
369+
370+
.. method:: getAttachment(artifact, filename)
371+
372+
Given a real artifact instance or the FormattedID of an existing artifact,
373+
obtain the attachment named by filename. If there is such an attachment,
374+
return an Attachment instance with hydration for Name, Size, ContentType, Content,
375+
CreationDate and the User that supplied the attachment.
376+
If no such attachment is present, return None
377+
378+
.. method:: getAttachmentNames(artifact)
379+
380+
Given a real artifact instance that is hydrated for at least the Attachments attribute,
381+
return the names (filenames) of the Attachments associated with the artifact.
382+
383+
.. method:: getAttachments(artifact)
384+
385+
Given a real artifact instance, return a list of Attachment records.
386+
Each Attachment record will look like a Rally WSAPI Attachment with
387+
the additional Content attribute that will contain the decoded AttachmentContent.
388+
389+
356390

357391
RallyRESTResponse
358392
=================

pyral/__init__.py

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

pyral/config.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
#
77
###################################################################################################
88

9-
__version__ = (0, 8, 12)
9+
__version__ = (0, 9, 1)
1010

1111
import datetime
1212
import os
@@ -19,7 +19,7 @@
1919
PROTOCOL = "https"
2020
SERVER = "rally1.rallydev.com"
2121
WEB_SERVICE = "slm/webservice/%s"
22-
WS_API_VERSION = "1.29"
22+
WS_API_VERSION = "1.30"
2323
JSON_FORMAT = ".js"
2424

2525
USER_NAME = "[email protected]"

pyral/context.py

Lines changed: 29 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@
88
#
99
###################################################################################################
1010

11-
__version__ = (0, 8, 12)
11+
__version__ = (0, 9, 1)
1212

13-
import sys
13+
import sys, os
1414
import time
1515
import socket
1616
import json
@@ -117,40 +117,52 @@ def check(self, server):
117117
and speaks Rally WSAPI, and establishes the default workspace and project for
118118
the user.
119119
"""
120-
target_host = server
120+
##
121+
## print " RallyContextHelper.check starting ..."
122+
## sys.stdout.flush()
123+
##
121124
socket.setdefaulttimeout(REQUEST_TIME_LIMIT)
122-
if IPV4_ADDRESS_PATT.match(server): # is server an IPV4 address?
125+
target_host = server
126+
127+
big_proxy = os.environ.get('HTTPS_PROXY', False)
128+
small_proxy = os.environ.get('https_proxy', False)
129+
proxy = big_proxy if big_proxy else small_proxy if small_proxy else False
130+
proxy_host = False
131+
if proxy:
132+
proxy_host, proxy_port = proxy.split(':')
133+
target_host = proxy_host or server
134+
135+
if IPV4_ADDRESS_PATT.match(target_host): # is server an IPV4 address?
123136
try:
124-
info = socket.gethostbyaddr(server)
137+
info = socket.gethostbyaddr(target_host)
125138
except socket.herror, msg:
126-
problem = "IP v4 address '%s' not valid or unreachable" % server
139+
problem = "IP v4 address '%s' not valid or unreachable" % target_host
127140
raise RallyRESTAPIError(problem)
128141
except Exception, msg:
129142
print "Exception detected: %s" % msg
130-
problem = "Exception detected trying to obtain host info for: %s" % server
143+
problem = "Exception detected trying to obtain host info for: %s" % target_host
131144
raise RallyRESTAPIError(problem)
132145

133146
# TODO: look for IPV6 type address also?
147+
134148
else:
135149
try:
136-
target_host = socket.gethostbyname(server)
150+
target_host = socket.gethostbyname(target_host)
137151
except socket.gaierror, msg:
138-
problem = "hostname '%s' non-existent or unreachable" % server
152+
problem = "hostname: '%s' non-existent or unreachable" % target_host
139153
raise RallyRESTAPIError(problem)
140154

141-
##
142-
## print " RallyContextHelper.check starting ..."
143-
## sys.stdout.flush()
144-
##
145155
# note the use of the _disableAugments keyword arg in the call
146156
user_name_query = 'UserName = "%s"' % self.user
147157
try:
148158
timer_start = time.time()
149-
response = self.agent.get('User', fetch=True, query=user_name_query, _disableAugments=True)
159+
response = self.agent.get('User', fetch=True, query=user_name_query,
160+
_disableAugments=True)
150161
timer_stop = time.time()
151162
except Exception, msg:
152163
if str(msg).startswith('404 Service unavailable'):
153-
raise RallyRESTAPIError("hostname '%s' non-existent or unreachable" % server)
164+
# TODO: discern whether we should mention server or target_host as the culprit
165+
raise RallyRESTAPIError("hostname: '%s' non-existent or unreachable" % server)
154166
else:
155167
raise
156168
elapsed = timer_stop - timer_start
@@ -162,6 +174,8 @@ def check(self, server):
162174
if response.status_code == 404:
163175
if elapsed >= float(REQUEST_TIME_LIMIT):
164176
problem = "Request timed out on attempt to reach %s" % server
177+
elif response.errors and 'Max retries exceeded with url' in response.errors[0]:
178+
problem = "Target Rally host: '%s' non-existent or unreachable" % server
165179
elif response.errors and 'NoneType' in response.errors[0]:
166180
problem = "Target Rally host: '%s' non-existent or unreachable" % server
167181
else:

pyral/entity.py

Lines changed: 75 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,10 @@
88
#
99
###################################################################################################
1010

11-
__version__ = (0, 8, 12)
11+
__version__ = (0, 9, 1)
1212

1313
import sys
14+
import re
1415

1516
from .restapi import hydrateAnInstance
1617
from .restapi import getResourceByOID
@@ -101,9 +102,6 @@ def __getattr__(self, name):
101102
if response.status_code != 200:
102103
raise UnreferenceableOIDError, ("%s OID %s" % (rallyEntityTypeName, self.oid))
103104
item = response.content[rallyEntityTypeName]
104-
#del item[u'Errors'] # this cruft from REST GET not actually part of the entity's attributes
105-
#del item[u'Warnings'] # ditto
106-
107105
##
108106
## print "calling hydrateAnInstance from the __getattr__ of %s for %s" % (name, self._type)
109107
## sys.stdout.flush()
@@ -126,13 +124,14 @@ def __getattr__(self, name):
126124

127125
class Subscription(Persistable): pass
128126

127+
class AllowedAttributeValue(Persistable): pass # only used in an AttributeDefinition
128+
class AllowedQueryOperator (Persistable): pass # only used in an AttributeDefinition
129+
# (for AllowedQueryOperators)
130+
129131
class DomainObject(Persistable):
130132
""" This is an Abstract Base class """
131133
pass
132134

133-
class AllowedAttributeValue(Persistable): pass # only used in an AttributeDefinition
134-
class AllowedQueryOperator (Persistable): pass # only used in an AttributeDefinition (for AllowedQueryOperators)
135-
136135
class User (DomainObject): pass
137136
class UserProfile (DomainObject): pass
138137
class Workspace (DomainObject): pass
@@ -142,8 +141,75 @@ class WorkspacePermission (UserPermission): pass
142141
class ProjectPermission (UserPermission): pass
143142

144143
class WorkspaceDomainObject(DomainObject):
145-
""" This is an Abstract Base class """
146-
pass
144+
"""
145+
This is an Abstract Base class, with a convenience method (details) that
146+
formats the attrbutes and corresponding values into an easily viewable
147+
mulitiline string representation.
148+
"""
149+
COMMON_ATTRIBUTES = ['_type',
150+
'ObjectID', '_ref', '_CreatedAt',
151+
'oid', 'ref', '_hydrated',
152+
'Name', 'Subscription', 'Workspace'
153+
]
154+
155+
def details(self):
156+
"""
157+
order we want to have the attributes appear in...
158+
159+
Class Name (aka _type)
160+
oid
161+
ref
162+
_ref
163+
_hydrated
164+
_CreatedAt
165+
ObjectID
166+
Name ** not all items will have this...
167+
Subscription (oid, Name)
168+
Workspace (oid, Name)
169+
170+
alphabetical from here on out
171+
"""
172+
tank = ['%s' % self._type]
173+
for attribute_name in self.COMMON_ATTRIBUTES[1:]:
174+
value = getattr(self, attribute_name)
175+
if 'pyral.entity.' not in str(type(value)):
176+
anv = ' %-16.16s : %s' % (attribute_name, value)
177+
else:
178+
mo = re.search(r' \'pyral.entity.(\w+)\'>', str(type(value)))
179+
if mo:
180+
cln = mo.group(1)
181+
anv = " %-16.16s : %-16.16s (OID %s Name: %s)" % \
182+
(attribute_name, cln + '.ref', value.oid, value.Name)
183+
else:
184+
anv = " %-16.16s : %s" % value
185+
tank.append(anv)
186+
tank.append("")
187+
other_attributes = set(self.attributes()) - set(self.COMMON_ATTRIBUTES)
188+
for attribute_name in sorted(other_attributes):
189+
value = getattr(self, attribute_name)
190+
if not isinstance(value, Persistable):
191+
anv = " %-16.16s : %s" % (attribute_name, value)
192+
else:
193+
mo = re.search(r' \'pyral.entity.(\w+)\'>', str(type(value)))
194+
if not mo:
195+
anv = " %-16.16s : %s" % (attribute_name, value)
196+
continue
197+
198+
cln = mo.group(1)
199+
anv = " %-16.16s : %-27.27s" % (attribute_name, cln + '.ref')
200+
if isinstance(value, Artifact):
201+
# also want the OID, FormattedID
202+
anv = "%s (OID %s FomattedID %s)" % (anv, value.oid, value.FormattedID)
203+
elif isinstance(value, User):
204+
# also want the className, OID, UserName, DisplayName
205+
anv = " %-16.16s : %s.ref (OID %s UserName %s DisplayName %s)" % \
206+
(attribute_name, cln, value.oid, value.UserName, value.DisplayName)
207+
else:
208+
# also want the className, OID, Name)
209+
anv = "%s (OID %s Name %s)" % (anv, value.oid, value.Name)
210+
tank.append(anv)
211+
return "\n".join(tank)
212+
147213

148214
class WorkspaceConfiguration(WorkspaceDomainObject): pass
149215
class Type (WorkspaceDomainObject): pass

pyral/hydrate.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
#
1010
###################################################################################################
1111

12-
__version__ = (0, 8, 12)
12+
__version__ = (0, 9, 1)
1313

1414
import types
1515
from pprint import pprint

pyral/rallyresp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
#
1111
###################################################################################################
1212

13-
__version__ = (0, 8, 12)
13+
__version__ = (0, 9, 1)
1414

1515
import sys
1616
import re

0 commit comments

Comments
 (0)