What is FogBugzPy?
FogBugzPy is a Python module that makes use of Beautiful Soup, a parsing library that makes it very easy to work with HTML and XML, to interact with the FogBugz XML API. We'll show you how to use Beautiful Soup insofar as it applies to using FogBugzPy, but if you're looking for more, you can read the Beautiful Soup documentation.
For instructions on installing FogBugzPy, see Installing Python and FogBugzPy.
The source code for FogBugzPy is available at https://developers.kilnhg.com/Repo/FogBugz/Group/FogBugzPy.
Conventions Used In This Guide
- fbSettings.py - These examples assume you have set up a fbSettings.py file. See Storing Configuration Settings in fbSettings.py.
-
Executing sample code - each bit of sample code can be copied into file called example.py that sits next to fbSettings.py. Once the code is copied, you can save example.py and execute it by typing the following from the command line:
1:
> python example.py
FogBugzPy and XML API Version
As of FogBugzPy version 1.0.6, you can specify an expected API version when you initialize the FogBugz object. If a backwards-incompatible change is ever made to the FogBugz API and your script specifies an earlier API version, FogBugzPy will raise an exception and alert you to the need to update your script. When there are backwards-compatible updates to the API, FogBugzPy will print a message to the console letting you know of the option to upgrade but will not throw an exception.
1: 2: 3: 4: 5: 6: |
>>> from fogbugz import FogBugz >>> import fbSettings >>> fb = FogBugz(fbSettings.URL, fbSettings.TOKEN, api_version=6) There is a newer version of the FogBugz API available. Please update to version 8 to avoid errors in the future >>> |
FogBugzPy and XML API Documentation
The FogBugz XML API Documentation is written in a way that is language and framework agnostic. This means that any language that can parse XML within a framework that can communicate over HTTP can use the XML API. However, without a frame of reference, getting started with the API can introduce a higher barrier to entry than if the API were written targeting a single language/framework. The FogBugzPy library and this development wiki seek to lower the barrier to entry to working with the FogBugz XML API.
When using FogBugzPy with Documentation, a cmd in the documentation becomes a method a FogBugz object in Python. Likewise, Arguments in the documentation become parameters to methods in Python. Let's look at an example.
The following is a clip of the Editing Cases section of the documentation:
We want to edit an existing FogBugz case, changing its title, so this is how we write that code in FogBugzPy:
1: 2: 3: 4: 5: 6: 7: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) fb.edit(ixBug=1, #fb.edit corresponds to cmd=edit sTitle="This is a new title") #ixBug and sTitle are both arguments |
Notice how the edit method in the Python script corresponds to cmd=edit in the documentation. ixBug and sTitle, which are arguments in the documentation, become method parameters in Python.
XML API Responses
When the FogBugz XML API returns a response, it returns XML.
Viewing the XML Response
For example, here's a sample script that stores the results of a search command and stores it in a variable called resp. It then prints the resp.
1: 2: 3: 4: 5: 6: 7: 8: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle") #store the results of the search in a #variable called resp print resp #print resp to see what it looks like |
When we execute, this script, we get:
1: 2: |
> python example.py <response><cases count="1"><case ixBug="1" operations="edit,assign,resolve,email,remind"><ixBug>1</ixBug><sTitle><![CDATA[This is a new title]]></sTitle></case></cases></response> |
That's fairly difficult to read. Let's use the prettify() method to make the output readable:
1: 2: 3: 4: 5: 6: 7: 8: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle") #store the results of the search in a #variable called resp print resp.prettify() #print resp using prettify() to see what it looks like |
Now when we execute the script, it's much easier to read:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
> python example.py <response> <cases count="1"> <case ixBug="1" operations="edit,assign,resolve,email,remind"> <ixBug> 1 </ixBug> <sTitle> <![CDATA[This is a new title]]> </sTitle> </case> </cases> </response> |
IMPORTANT! - In older versions of FogBugzPy, elements and columns are always lower case. Even though columns and attributes are camel cased (e.g. ixBug) in the Documentation, earlier versions of Beautiful Soup convert all elements to lower case. Thus, even though you will request ixBug, the response will always need to be accessed in lowercase, e.g. ixbug. As of FogBugzPy v.1.0.5, this is no longer the case; elements and columns appear in camelCase in the response.
Extracting Data From XML Elements
When you get a response back, you usually want to do something with that data. With Beautiful Soup, when you want to parse XML data, each child element becomes a Python attribute of the parent object. In the XML output above, we can see that response is a parent of cases, which is a parent of case, which is a parent of sTitle, so if we want to get the sTitle value the response, we could use resp.cases.case.sTitle
. Let's print it and see what we get:
1: 2: 3: 4: 5: 6: 7: 8: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle") #store the results of the search in a #variable called resp print resp.cases.case.sTitle |
When executed:
1: 2: |
> python example.py <sTitle><![CDATA[This is a new title]]></sTitle> |
That returns the whole element, which isn't perhaps what we wanted. If we want just the text inside the sTitle element without the element itself, we can add .string
to our code:
1: 2: 3: 4: 5: 6: 7: 8: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle") #store the results of the search in a #variable called resp print resp.cases.case.sTitle.string #adding .string gives us just text |
Now, when executed:
1: 2: |
> python example.py This is a new title |
For nearly all operations, you can use case.columnName.string
to get the data you want. We'll explore how to work with various data types in an upcoming section.
Extracting Data From XML Attributes
In the example above, we looked at extracting data from the <sTitle>...</sTitle>
element. What if we want to get the value associated with the ixBug value of case, e.g. <case ixBug="1">
? We can treat the tag (element) object as if it were a dictionary, e.g. case['ixBug']
. The following example shows the difference between accessing the text within a tag versus accessing its attribute:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,stitle") print resp.cases.case.sTitle.string # This prints the text within the <sTitle> element print resp.cases.case['ixBug'] # This prints the ixBug attribute of the <case> # element, e.g. <case ixBug="1"> |
Working with Data Types
You can use the syntax we described above for extracting data from XML Elements, e.g. case.columnName.string
, for most of your needs. However, this will always return some form of string value. Python will try to infer types when it can, but sometimes you need to explicitly work with other data types.
Integers
Use int(expression)
to explicitly convert to an integer, e.g. int(resp.cases.case.ixBug.string)
. Note how we still have to use .string
with the ixBug element.
Strings
Although the value for each element is a string, text elements (such as sTitle or sEvent) will be wrapped in CDATA by the XML API. FogBugzPy v.1.0.5 handles CDATA automatically and these fields can be treated like regular strings; however, this is a bit of annoyance in older versions of FogBugzPy. You can work around this issue by encoding your strings as UTF-8, using .encode('UTF-8')
. In the following example, look at the difference in string replacement when you lack encoding versus including it:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle") bug = resp.cases.case.ixbug.string sTitle = resp.cases.case.stitle.string print "%s: %s" % (bug, sTitle) #look at how stitle prints, wrapped in CDATA # 1: <![CDATA[This is a new title]]> sTitle = resp.cases.case.stitle.text #returns the text contents of the field print "%s: %s" % (bug, sTitle) #this time, CDATA is not present # 1: This is a new title |
As executed:
1: 2: 3: |
> python example.py 1: <![CDATA[This is a new title]]> 1: This is a new title |
Floating-point numbers
You can convert to a floating-point number using either float(expression)
or Decimal(expression)
. In most cases, float is fine. In the following example, we've added an Estimate: Current to Case 1 so that we can use hrsCurrEst via the XML API.
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
from fogbugz import FogBugz import fbSettings from decimal import Decimal #you'll need to import Decimal if you want to use it fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle,hrsCurrEst") dblHrsCurrEst = float(resp.cases.case.hrsCurrEst.string) #convert to a float decHrsCurrEst = Decimal(resp.cases.case.hrsCurrEst.string)#convert to a decimal print dblHrsCurrEst print decHrsCurrEst |
As executed:
1: 2: 3: |
> python example.py 3.4 3.4 |
Date and Time Values
All Date and Time values returned from FogBugz are provided as UTC. You'll need to use the datetime module with the strptime method with the format '%Y-%m-%dT%H:%M:%SZ' to convert from strings to datetime values. In the following example, we'll show how to work with datetime and timedelta objects:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: |
from fogbugz import FogBugz import fbSettings from datetime import datetime, timedelta #these are needed to work with date/time values fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle,dtOpened") #use datetime.strptime(element.string, '%Y-%m-%dT%H:%M:%SZ') to convert to a datetime dtOpenedUtc = datetime.strptime(resp.cases.case.dtOpened.string,'%Y-%m-%dT%H:%M:%SZ') print "This is what case.dtOpened.string looks like in XML: %s" % resp.cases.case.dtOpened.string print "We can see that dtOpenedUtc is a datetime type: %s" % type(dtOpenedUtc) print "This is what dtOpenedUtc looks like printed: %s" % dtOpenedUtc year = dtOpenedUtc.year print "dtOpenedUtc.year tells us the year: %d" % year #this will make a time delta object so we can find the difference in times tdSinceToday = datetime.utcnow() - dtOpenedUtc days = tdSinceToday.days print "This case was opened %d days ago" % days |
As executed:
1: 2: 3: 4: 5: 6: |
PS C:\code\Faciliscript> python example.py This is what case.dtOpened.string looks like in XML: 2011-02-17T14:47:07Z We can see that dtOpenedUtc is a datetime type: <type 'datetime.datetime'> This is what dtOpenedUtc looks like printed: 2011-02-17 14:47:07 dtOpenedUtc.year tells us the year: 2011 This case was opened 223 days ago |
Boolean values
Boolean values are returned as strings, true or false, with an f (for flag) prefixing the variable name. Test to see if a variable is True or False by comparing to the string values 'true'
or 'false'
:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q=1,cols="ixBug,sTitle,fOpen") if resp.cases.case.fOpen.string == 'true': print "This case is open." else: print "This case is closed." |
As executed:
1: 2: |
PS C:\code\misc> python .\example.py This case is open. |
NOTE: On some commands (e.g. listPeople), there are boolean arguments that require a 1 (for true) or 0 (for false).
Looping Over Collections
Looping over multiple elements in a collection is easy. With Beautiful Soup, we can use either elements.childGenerator()
or elements.findAll('element')
to generate a collection. From there, we simply use Python's for item in collection:
syntax.
Here's an example that loops over a list of projects using elements.childGenerator()
:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.listProjects() for project in resp.projects.childGenerator(): print "%s: %s" % (project.ixProject.string, project.sProject.string.encode('UTF-8')) |
As executed:
1: 2: 3: 4: |
> python .\example.py 1: Inbox 2: FogBugz 3: Kiln |
Here's another example that loops over a list of cases using for item in collection:
:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: |
from fogbugz import FogBugz import fbSettings fb = FogBugz(fbSettings.URL, fbSettings.TOKEN) resp = fb.search(q='assignedTo:"me" status:"Active"', cols="ixBug,sTitle", max=10) for case in resp.cases.findAll('case'): print "%s: %s" % (case.ixBug.string, case.sTitle.string) |
As executed:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: |
> python .\example.py 1: This is a new title 152: "Welcome to FogBugz" Sample Case 151: test 150: (Untitled) 149: test 147: I would like the program to say Hello Earth instead 146: Adding a reminder! 26: (Untitled) 3: Project: Person Client: Hello This is for the other one 145: Test |