Ruy Adorno avatar Ruy Adorno committed 4e78054

* Merged files into a single one
* Cleaned redmine class
* Implementing add time method

Comments (0)

Files changed (1)

 
 import urllib
 import urllib2
+import os
+from urllib2 import URLError
+from argparse import ArgumentParser
+from argparse import Action
 from xml.dom import minidom, getDOMImplementation
 
-        
+OPTION_CHAR = "="
+DEFAULT_CONFIG_FILE = "~/.jazzrc"
+
+def parse_config():
+    f = open(os.path.expanduser(DEFAULT_CONFIG_FILE))
+    options = {}
+    for line in f:
+        if OPTION_CHAR in line:
+            option, value = line.split(OPTION_CHAR, 1)
+            option = option.strip()
+            value = value.strip()
+            options[option] = value
+    f.close()
+    return options
+
+class Jazz:
+
+    def __init__(self):
+        self.parse_config_file()
+        self.start_parser()
+        self.parse_arguments()
+        self.start_redmine()
+        self.parse_subcommands()
+
+    def parse_config_file(self):
+        try:
+            options = parse_config()
+            self.redmineurl = options['url']
+            self.redminekey = getattr(options, 'key', None)
+            self.redmineuser = getattr(options, 'username', None)
+            self.redminepass = getattr(options, 'password', None)
+            self.default_project = getattr(options, 'project', None)
+            self.default_activity = getattr(options, 'activity', None)
+            self.default_message = getattr(options, 'message', None)
+            self.no_config_file = False
+        except IOError:
+            self.no_config_file = True
+            print "No configuration file found at: "+os.path.expanduser(DEFAULT_CONFIG_FILE)
+
+    def start_redmine(self):
+        try:
+            if self.redminekey:
+                self.redmine = Redmine(self.redmineurl, self.redminekey)
+            else:
+                self.redmine = Redmine(self.redmineurl, username=self.redmineuser, password=self.redminepass)
+                
+        except URLError:
+            print "ERROR: Can't connect to server, check if the redmine URL is correct"
+            self.parser.exit()
+
+    def start_parser(self):
+        self.parser = ArgumentParser(description="Provides a basic interface to access redmine.", prog="jazz")
+        self.parser.add_argument("-u", "--url", help="Redmine url")
+        self.parser.add_argument("-k", "--key", help="Your user key")
+        self.subparsers = self.parser.add_subparsers(help="Available commands:")
+        self.li = self.subparsers.add_parser("li", help="List issues from redmine.")
+        self.li.add_argument("-p", "--project", help="Specify a project to get the issues.")
+        self.li.set_defaults(func=self.list_issues)
+        self.add = self.subparsers.add_parser("add", help="Create a new time entry.")
+        self.add.add_argument("-p", "--project", help="Specify a project to get the issues.")
+        self.add.add_argument("-i", "--issue", help="The id of a specific issue to add the time.")
+        self.add.add_argument("-t", "--hours", "--time", help="The amount of time spent (in hours).")
+        self.add.add_argument("-a", "--activity", help="The id of the activity")
+        self.add.add_argument("-m", "--message", help="A short description for the entry (255 chars max)")
+        self.add.set_defaults(func=self.add_time)
+        self.log = self.subparsers.add_parser("log", help="Get logged time in the system")
+        self.log.set_defaults(func=self.get_log)
+
+    def parse_arguments(self):
+        self.args = self.parser.parse_args()
+        if self.args.url:
+            self.redmineurl = self.args.url
+        if self.args.key:
+            self.redminekey = self.args.key
+        if self.no_config_file and not self.args.url and not self.args.key:
+            print "ERROR: You need to specify an redmine url and key in order to use this program"
+            self.parser.exit()
+
+    def parse_subcommands(self):
+        self.args.func(self.args)
+
+    def list_issues(self, args):
+        if not args.project:
+            self.print_issues(self.redmine.getIssues())
+        else:
+            self.print_issues(self.redmine.getProject(args.project).getIssues())
+
+    def print_issues(self, issues):
+        print "\nOpen Issues:\n"
+        for issue in issues:
+            print "Issue#: "+issue['id']+" - "+issue['subject']+" - "+issue["done_ratio"]+"% done"
+
+    def add_time(self, args):
+        print "\nAdding time:\n"
+        project_id = getattr(args, 'project', self.default_project)
+        issue_id = getattr(args, 'issue', None)
+        if not args.hours or args.time:
+            print "ERROR: You need to specify the amount of time spent!"
+            self.parser.exit()
+        hours = getattr(args, 'hours', args.time)
+        activity_id = getattr(args, 'activity', self.default_activity)
+        message = getattr(args, 'message', self.default_message)
+        self.redmine.createTimeEntry(hours, activity_id, message, issue_id, project_id)
+
+    def get_log(self, args):
+        pass
+
+#Entry point (maybe it's ugly?)
+Jazz()
+
 
 class Redmine:
-    '''Class to interoperate with a Redmine installation using the REST web services.
-    instance = Redmine(url, [key=strKey], [username=strName, password=strPass] )
     
-    url is the base url of the Redmine install ( http://my.server/redmine )
-    
-    key is the user API key found on the My Account page for the logged in user
-        All interactions will take place as if that user were performing them, and only
-        data that that user can see will be seen
-
-    If a key is not defined then a username and password can be used
-    If neither are defined, then only publicly visible items will be retreived  
-    '''
-    
-    def __init__(self, url, key=None, username=None, password=None ):
+    def __init__(self, url, key=None, username=None, password=None):
         self.__url = url
         self.__key = key
         self.projects = {}
         self.projectsID = {}
         self.projectsXML = {}
-        
         self.issuesID = {}
         self.issuesXML = {}
         
-        # Status ID from a default install
         self.ISSUE_STATUS_ID_NEW = 1
         self.ISSUE_STATUS_ID_RESOLVED = 3
         self.ISSUE_STATUS_ID_FEEDBACK = 4
             self.__key = None
         
         if not password:
-            password = '12345'  #the same combination on my luggage!  (dummy value)
+            password = ''
         
         if( username and password ):
-            #realm = 'Redmine API'
-            # create a password manager
             password_mgr = urllib2.HTTPPasswordMgrWithDefaultRealm()
-
             password_mgr.add_password(None, url, username, password )
             handler = urllib2.HTTPBasicAuthHandler( password_mgr )
 
-            # create "opener" (OpenerDirector instance)
             self.__opener = urllib2.build_opener( handler )
-
-            # set the opener when we fetch the URL
             self.__opener.open( url )
-
-            # Install the opener.
             urllib2.install_opener( self.__opener )
             
         else:
             if not key:
-                pass
-                #raise TypeError('Must pass a key or username and password')
+                raise TypeError('Must pass a key or username and password')
         
     class Project:
-        '''Object returned by Redmine getProject calls
-           redmine is the redmine object.
-           objXML is the xml object containing the object data'''
-        
+
         def __init__(self, redmine, objXML ):
             self.__redmine = redmine
             self.objXML = objXML
             self.id = self.data[ 'identifier' ]
 
         def newIssue(self, subject, description='', priorityID=None, trackerID=None, assigned_to_id=None, Xdata=None ):
-            '''Create a new issue for this project.  Unfortunately, there is no easy way to 
-               discover the valid values for priorityID and trackerID'''
-               
             if Xdata:
                 data = Xdata.copy()
             else:
             return my_issues
 
     class User:
+
         def __init(self, redmine, objXML):
             self.__redmine = redmine
             self.objXML = objXML
             #TODO - not yet avilable on 1.1 rest api
             pass;
 
-    # extend the request to handle PUT command
     class PUT_Request(urllib2.Request):
         def get_method(self):
             return 'PUT'
 
-    # extend the request to handle DELETE command
     class DELETE_Request(urllib2.Request):
         def get_method(self):
             return 'DELETE'
 
     def open(self, page, parms=None, objXML=None, HTTPrequest=None ):
-        '''Opens a page from the server with optional XML.  Returns an XML object'''
         if not parms:
             parms={}
             
         if parms:
             urldata = '?' + urllib.urlencode( parms )
         
-        
         fullUrl = self.__url + '/' + page
         
         # register this url to be used with the opener
         if self.__opener:
             self.__opener.open( fullUrl )
             
-        #debug
-        #print fullUrl + urldata
-        
         # Set up the request
         if HTTPrequest:
             request = HTTPrequest( fullUrl + urldata )
             return response.read()
     
     def get(self, page, parms=None ):
-        '''Gets an XML object from the server - used to read Redmine items.'''
         return self.open( page, parms )
     
     def post(self, page, encoded_params ):
         return request
     
     def put(self, page, objXML, parms=None ):
-        '''Puts an XML object on the server - used to update Redmine items.  Returns nothing useful.'''
         return self.open( page, parms, objXML, HTTPrequest=self.PUT_Request )
     
     def delete(self, page ):
-        '''Deletes a given object on the server - used to remove items from Redmine.  Use carefully!'''
         return self.open( page, HTTPrequest=self.DELETE_Request )
     
     def parseRedmineXML(self, objXML, container ):
-        '''parses the Redmine XML into a python dict.  Returns data within the first container found.'''
-        #todo: correctly parse nested child nodes
-        
         d = {}
         pXML = objXML.getElementsByTagName(container)[0]
         for child in pXML.childNodes:
             if child.hasChildNodes():
                 d[child.nodeName] = child.firstChild.nodeValue
-                
         return d
         
     def XMLaddkeyval(self, xmlDoc, xmlNode, key, value):
         xmlNode.appendChild( xmlChild )
 
     def XMLcreatedoc(self, tag ):
-        '''returns a new XML document with the given tag as the outermost container '''
         return getDOMImplementation().createDocument(None, tag, None)
     
     def dict2XML(self, tag, dict ):
-        '''returns a new XML document with the given tag and the dict encoded within '''
         xml = self.XMLcreatedoc( tag )
         for key in dict:
             self.XMLaddkeyval( xml, xml.firstChild, key, dict[key] )
         return xml
     
     def parseProject(self, objXML ):
-        '''parses project data from an XML object'''
         projectDict = self.parseRedmineXML( objXML, 'project' )
         
         self.projects[ projectDict['identifier'] ] = projectDict
         return projectDict
     
     def parseIssue(self, objXML ):
-        '''parses issue data from an XML object'''
         issue = self.parseRedmineXML( objXML, 'issue' )
         
         self.issuesID[ issue['id'] ] = issue
         return issue
         
     def getProject(self, projectIdent ):
-        '''returns a dictionary for the given project name'''
         #return self.parseProject( self.get('projects/'+projectIdent+'.xml') )
         return self.Project( self, self.get('projects/'+projectIdent+'.xml') )
 
     def getIssue(self, issueID ):
-        '''returns a dictionary for the given issue'''
         return self.parseIssue( self.get('issues/'+str(issueID)+'.xml') )
 
     def _getIssuesXML(self, quantity=100):
         return my_issues
         
     def newIssueFromDict(self, dict ):
-        '''creates a new issue using fields from the passed dictionary'''
         #xml = self.dict2XML( 'issue', dict )
         #return self.parseIssue( self.post( 'issues.xml', xml ) )
         pass
     
     def updateIssueFromDict(self, ID, dict ):
-        '''updates an issue with the given ID using fields from the passed dictionary'''
         xml = self.dict2XML( 'issue', dict )
         return self.put( 'issues/'+str(ID)+'.xml', xml )
 
     def deleteIssue(self, ID ):
-        '''delete an issue with the given ID.  This can't be undone - use carefully!
-        Note that the proper method of finishing an issue is to update it to a closed state.'''
         return self.delete( 'issues/'+str(ID)+'.xml' )
         
     def closeIssue(self, ID ):
-        '''close an issue by setting the status to self.ISSUE_STATUS_ID_CLOSED'''
         return self.updateIssueFromDict( ID, {'status_id':self.ISSUE_STATUS_ID_CLOSED} )
         
     def resolveIssue(self, ID ):
-        '''close an issue by setting the status to self.ISSUE_STATUS_ID_RESOLVED'''
         return self.updateIssueFromDict( ID, {'status_id':self.ISSUE_STATUS_ID_RESOLVED} )
         
 
-    def createTimeEntry(self, hours, activity_id, comments, issue_id="", project_id=""):
+    def createTimeEntry(self, hours, activity_id, comments="", issue_id=None, project_id=""):
         post_xml = "<?xml version='1.0' encoding='UTF-8'?><time_entry>"
-        post_xml += "<issue_id>"+issue_id+"</issue_id>"
+        if issue_id:
+            post_xml += "<issue_id>"+issue_id+"</issue_id>"
+        else:
+            post_xml += "<project_id>"+project_id+"</project_id>"
         post_xml += "<hours>"+hours+"</hours>"
         post_xml += "<activity_id>"+activity_id+"</activity_id>"
         post_xml += "<comments>"+comments+"</comments>"
         post_xml += "</time_entry>"
         encoded_content = post_xml
         return self.post("time_entries.xml", encoded_content)
-
-    def encodeDictIntoString(self, original_dict, main_key):
-        strDict = ""
-        for k, v in original_dict.items():
-            strDict += main_key+"["+str(k)+"]="+str(v)+"&"
-        return strDict
Tip: Filter by directory path e.g. /media app.js to search for public/media/app.js.
Tip: Use camelCasing e.g. ProjME to search for ProjectModifiedEvent.java.
Tip: Filter by extension type e.g. /repo .js to search for all .js files in the /repo directory.
Tip: Separate your search with spaces e.g. /ssh pom.xml to search for src/ssh/pom.xml.
Tip: Use ↑ and ↓ arrow keys to navigate and return to view the file.
Tip: You can also navigate files with Ctrl+j (next) and Ctrl+k (previous) and view the file with Ctrl+o.
Tip: You can also navigate files with Alt+j (next) and Alt+k (previous) and view the file with Alt+o.