Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New functionality from all forks #1

Open
wants to merge 9 commits into
base: master
Choose a base branch
from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
*.pyc
96 changes: 90 additions & 6 deletions pivotaltracker/_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,25 @@ def __init__(self, token, secure=True, parse_xml=True):
these options by setting 'secure' or 'parse_xml' to False"""
self.__token = token
protocol = "https" if secure else "http"
self.__base_url = "%(protocol)s://www.pivotaltracker.com/services/v2/" % dict(protocol=protocol)
self.__base_url = "%(protocol)s://www.pivotaltracker.com/services/v4/" % dict(protocol=protocol)
self.__parse_xml = parse_xml


def get_activity(self,limit=None,occurred_since_date=None):
"""gets activity for all projects"""
params = {}
if limit:
params["limit"] = limit
if occurred_since_date:
params["occurred_since_date"] = occurred_since_date
if params:
# we have parameters to send
encoded_params = urllib.urlencode(params)
return self.__remote_http_get("activities?%s" % (encoded_params))
else:
# no arguments, get all stories
return self.__remote_http_get("activities" )
return self.__remote_http_get("activities")

def get_project(self, project_id):
"""gets a project from the tracker"""
return self.__remote_http_get("projects/%s" % project_id)
Expand All @@ -28,10 +44,15 @@ def get_all_projects(self):
def get_story(self, project_id, story_id):
"""gets an individual story"""
return self.__remote_http_get("projects/%s/stories/%s" % (project_id, story_id))

def get_story_activity(self, project_id, story_id):
"""get story activities"""
return self.__remote_http_get("projects/%s/stories/%s/activities" % (project_id, story_id))

def get_stories(self, project_id, query=None, limit=None, offset=None):
"""gets stories from a project. These stories can be filtered via 'query', and
paginated via 'limit' and 'offset'"""
# WARNING The name filter just doesn't work due to some PT api bug. You'll either get back nothing or everthing, irrespective of search terms.
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you bring this up with the Pivotal team?

params = {}
if query:
params["filter"] = query
Expand All @@ -47,6 +68,30 @@ def get_stories(self, project_id, query=None, limit=None, offset=None):
# no arguments, get all stories
return self.__remote_http_get("projects/%s/stories" % project_id)

def get_task(self, project_id, story_id, task_id):
"""gets a specific task associated with an individual story"""
return self.__remote_http_get("projects/%s/stories/%s/tasks/%s" % (project_id, story_id, task_id))

def get_tasks(self, project_id, story_id):
"""gets the tasks associated with an individual story"""
return self.__remote_http_get("projects/%s/stories/%s/tasks" % (project_id, story_id))

def add_task(self, project_id, story_id, description, complete=None):
"""adds a task to a story"""
xml = self.__get_task_xml(description, complete)
data = xml.toxml()
return self.__remote_http_post("projects/%s/stories/%s/tasks" % (project_id, story_id), data=data)

def update_task(self, project_id, story_id, task_id, description, complete=None):
"""updates a task on a story"""
xml = self.__get_task_xml(description, complete)
data = xml.toxml()
return self.__remote_http_post("projects/%s/stories/%s/tasks/%s" % (project_id, story_id, task_id), data=data)

def delete_task(self, project_id, story_id, task_id):
"""deletes a task in a story"""
return self.__remote_http_delete("projects/%s/stories/%s/tasks/%s" % (project_id, story_id, task_id))

def get_iterations(self, project_id, limit=None, offset=None):
"""gets iterations from a project. These iterations can be paginated via 'limit' and 'offset'"""
return self.__iterations_request_helper(sub_url="iterations", project_id=project_id, limit=limit, offset=offset)
Expand All @@ -67,16 +112,23 @@ def get_backlog_iterations(self, project_id, limit=None, offset=None):

def add_story(self, project_id, name, description, story_type, requested_by=None, estimate=None, current_state=None, labels=None):
"""adds a story to a project"""
xml = self.__get_story_xml(project_id, name, description, requested_by, story_type, estimate, current_state, labels)
#WARNING: Wait for 3 seconds after adding a story before trying to retrieve it to let the API catch up
xml = self.__get_story_xml(name, description, requested_by, story_type, estimate, current_state, labels)
data = xml.toxml()
return self.__remote_http_post("projects/%s/stories" % project_id, data=data)

def update_story(self, project_id, story_id, name=None, description=None, requested_by=None, story_type=None, estimate=None, current_state=None, labels=None):
"""updates a story in a project"""
xml = self.__get_story_xml(project_id, name, description, requested_by, story_type, estimate, current_state, labels)
xml = self.__get_story_xml(name, description, requested_by, story_type, estimate, current_state, labels)
data = xml.toxml()
return self.__remote_http_put("projects/%s/stories/%s" % (project_id, story_id), data=data)

def move_story(self, project_id, story_id, target_story_id, precedence=None):
"""moves a story before (default) or after another story"""
xml = self.__get_move_xml(target_story_id, precedence)
data = xml.toxml()
return self.__remote_http_post("projects/%s/stories/%s/moves" % (project_id, story_id), data=data)

def delete_story(self, project_id, story_id):
"""deletes a story in a project"""
return self.__remote_http_delete("projects/%s/stories/%s" % (project_id, story_id))
Expand All @@ -102,7 +154,7 @@ def deliver_all_finished_stories(self, project_id):
for live testing."""
return self.__remote_http_put("projects/%s/stories/deliver_all_finished" % project_id)

def __get_story_xml(self, project_id, name, description, requested_by, story_type, estimate, current_state, labels):
def __get_story_xml(self, name, description, requested_by, story_type, estimate, current_state, labels):

# build XML elements
elements = []
Expand Down Expand Up @@ -131,6 +183,38 @@ def __get_story_xml(self, project_id, name, description, requested_by, story_typ
xml = minidom.parseString(xml_string.strip())
return xml

def __get_task_xml(self, description, complete):

# build XML elements
elements = []
if description is not None:
elements.append("<description>%s</description>" % description)
if complete is not None:
elements.append("<complete type=\"boolean\">%s</complete>" % complete)
else:
elements.append("<complete type=\"boolean\">false</complete>")

# build XML
xml_string = "<task>%s</task>" % "".join(elements)
xml = minidom.parseString(xml_string.strip())
return xml

def __get_move_xml(self, target_story_id, precedence):

# build XML elements
elements = []
if target_story_id is not None:
elements.append("<target>%s</target>" % target_story_id)
if precedence is not None:
elements.append("<move>%s</move>" % precedence)
else:
elements.append("<move>before</move>")

# build XML
xml_string = "<move>%s</move>" % "".join(elements)
xml = minidom.parseString(xml_string.strip())
return xml

def __iterations_request_helper(self, sub_url, project_id, limit, offset):
params = {}
if limit is not None:
Expand Down Expand Up @@ -187,7 +271,7 @@ def parse_by_type(node):
return value

elif obj_type == "commalist":
value = node.childNodes[0].wholeText.strip()
value = ''#node.childNodes[0].wholeText.strip()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for using empty string here?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before I merge this in, I'd like to confirm the reasoning behind this change - was this some stray debugging that accidentally got committed?

return value.split(",")

elif obj_type == "dictionary":
Expand Down