root/hacks/trunk/basecamp_daily_report.py

Revision 4, 8.0 kB (checked in by verbosus, 3 years ago)

Added basecamp_daily_report.py: generates activity reports from one Basecamp project to another

  • Property svn:eol-style set to native
  • Property svn:executable set to *
  • Property svn:keywords set to Id
Line 
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3"""Daily Report of Basecamp activity
4
5Logs into a Basecamp site and posts a daily activity report from one
6(source) project to another (target) project.
7
8It is meant to run as a cronjob, but may be invoked manually from the
9command line, if needed, thus:
10
11$ python basecamp_daily_report.py \\
12  --hostname=hostname \\
13  --username=username \\
14  --password=password \\
15  --source="Source Project" \\
16  --target="Target Project" \\
17  --category="Target Category" \\
18  --subject="Daily Report"
19
20Requires ElementTree [1], httplib2 [2] and Basecamp Wrapper [3].
21
22[1] http://effbot.org/zone/element-index.htm
23[2] httplib2: http://bitworking.org/projects/httplib2/
24[3] Basecamp Wrapper: http://homework.nwsnet.de/products/79/
25"""
26
27__author__ = "Antonio Cavedoni <antonio@cavedoni.org>"
28__svnid__ = "$Id$"
29
30import sys
31import time
32import locale
33import datetime
34import elementtree.ElementTree as ET
35import httplib2
36import getopt
37from basecamp import Basecamp
38
39# Python 2.3 on Windows barks on locale names with underscores
40# and Python 2.4.1 on OS X barks on them *without* underscores, oh well
41if sys.platform == 'win32' and sys.version_info[0] >= 2 \
42        and sys.version_info[1] <= 3:
43    loc = 'it'
44else:
45    loc = 'it_IT'
46# this stuff shouldn’t be here, this is a library, we’re not
47# supposed to mess with the environment locale settings
48locale.setlocale(locale.LC_ALL, loc)
49
50HOSTNAME = ''
51USERNAME = ''
52PASSWORD = ''
53SOURCE_PROJECT = ''
54TARGET_PROJECT = ''
55TARGET_CATEGORY = ''
56SUBJECT = ''
57
58def iso2datetime(isotime):
59    """Parses an ISO datetime like 2006-01-19T12:47:20Z
60    and returns a datetime with no timezone
61    (Basecamp claims to return UTC dates)
62    """
63    return datetime.datetime(*time.strptime(isotime, '%Y-%m-%dT%H:%M:%SZ')[:6])
64
65class Post:
66    def __init__(self, xml_tree):
67        self.id = int(xml_tree.find('id').text)
68        self.title = xml_tree.find('title').text
69        if xml_tree.find('body'): self.body = xml_tree.find('body').text
70        self.posted_on = iso2datetime(xml_tree.find('posted-on').text)
71        #if xml_tree.find('category'):
72        #    self.category_id = int(xml_tree.find('category.id').text)
73        #    self.category_name = xml_tree.find('category.name').text
74        self.attachments_count = int(xml_tree.find('attachments-count').text)
75
76    def __repr__(self):
77        return '<Post: %d from %s>' % \
78            (self.id, self.posted_on.strftime('%Y-%m-%d'))
79
80class TodoItem:
81    def __init__(self, xml_tree):
82        self.id = int(xml_tree.find('id').text)
83        self.content = xml_tree.find('content').text
84        self.created_on = iso2datetime(xml_tree.find('created-on').text)
85        if xml_tree.find('completed').text == 'true':
86            self.completed = True
87            self.completed_on = iso2datetime(xml_tree.find('completed-on').text)
88        else:
89            self.completed = False
90            self.completed_on = datetime.datetime(1950, 12, 12)
91
92
93    def __repr__(self):
94        return '<TodoItem: %d>' % self.id
95
96def get_project_id(name):
97    projects = ET.fromstring(bc.projects())
98    for proj in projects.getiterator('project'):
99        if proj.find('name').text == name:
100            return int(proj.find('id').text)
101
102def get_messages(proj):
103    messages = ET.fromstring(bc.message_archive(proj))
104    for m in messages.getiterator('post'):
105        yield Post(m)
106
107def get_todo_list_ids():
108    todo_lists = ET.fromstring(bc.todo_lists(source_project_id))
109    for todo_list in todo_lists.getiterator('todo-list'):
110        yield int(todo_list.find('id').text)
111
112def get_todo_items():
113    for todo_list_id in get_todo_list_ids():
114        todo_list = ET.fromstring(bc.todo_list(todo_list_id))
115        for todo_item in todo_list.getiterator('todo-item'):
116            yield TodoItem(todo_item)
117
118def get_today_todo_items():
119    for todo_item in get_todo_items():
120        if ((todo_item.created_on - datetime.datetime.utcnow()) \
121            > datetime.timedelta(days=-1)) or \
122            ((todo_item.completed_on - datetime.datetime.utcnow()) \
123             > datetime.timedelta(days=-1)):
124            yield todo_item   
125
126def get_today_messages():
127    for message in get_messages(get_project_id(SOURCE_PROJECT)):
128        # if posted in the last week
129        if (message.posted_on - datetime.datetime.utcnow()) \
130            > datetime.timedelta(days=-1):
131            yield message
132
133def get_target_category_id(name):
134    categories = ET.fromstring(bc.message_categories(get_project_id(TARGET_PROJECT)))
135    for cat in categories.getiterator('post-category'):
136        if cat.find('name').text == name:
137            return int(cat.find('id').text)
138
139def posts_today():
140    post = []
141    messages = []
142    for m in get_today_messages():
143        messages.append(m)
144    if len(messages) != 0:
145        post.append("I messaggi delle ultime 24 ore:\n")
146        for m in messages:
147            post.append("* %s" % m.title.encode('utf-8'))
148    todo_items = []
149    for i in get_today_todo_items():
150        todo_items.append(i)
151
152    if (len(todo_items) + len(messages)) == 0:
153        print "There was no activity in the past 24 hours, abort."
154        sys.exit(0)
155
156    post.append("")
157    if len(todo_items) != 0:
158        post.append("I todo-items delle ultime 24 ore:\n")
159        for item in todo_items:
160            if item.completed:
161                # @@HACK! We’re doing decode('latin1') because we
162                # know that the locale will be 'it' or 'it_IT',
163                # but that is hardcoded and… well, just wrong
164                post.append("* -%s- _(completato %s)_" % (
165                item.content.encode('utf-8'),
166                item.completed_on.strftime(
167                    '%A %d/%m/%Y').decode('latin1').lower().encode('utf-8')
168                ))
169            else:
170                post.append("* %s _(creato %s)_" % (
171                item.content.encode('utf-8'),
172                item.created_on.strftime(
173                    '%A %d/%m/%Y').decode('latin1').lower().encode('utf-8')
174                ))
175    return '\n'.join(post)
176
177def create_message(proj_id, category_id, subject, message):
178    """Brute-force attempt to create a new Basecamp post, since the
179    method in the Python Basecamp library is not working properly
180    """
181    POST_CREATE_URI = "http://%s/projects/%s/msg/create" % \
182        (HOSTNAME, proj_id)
183    h = httplib2.Http()
184    h.add_credentials(USERNAME, PASSWORD)
185    request_body = """
186<request>
187  <post>
188    <category-id>%s</category-id>
189    <title>%s</title>
190    <body>%s</body>
191    <extended-body/>
192    <use-textile>1</use-textile>
193    <private>0</private>
194  </post>
195</request>
196""" % (category_id, subject, message)
197    response, content = h.request(POST_CREATE_URI, 'POST', body=request_body, 
198              headers={'Accept': 'application/xml', 
199                       'Content-type': 'application/xml'}
200              )
201
202def usage():
203    print __doc__
204
205if __name__ == "__main__":
206    try:
207        opts, args = getopt.getopt(
208            sys.argv[1:], 
209            "hupstcj", 
210            ("hostname=", "username=", "password=", "source=", "target=",
211             "category=", "subject=")
212            )
213    except getopt.GetoptError:
214        usage()
215        sys.exit(2)
216    if not opts:
217        usage()
218    else:
219        for o, a in opts:
220            if o in "--hostname":
221                HOSTNAME = a
222            elif o in "--username":
223                USERNAME = a
224            elif o in "--password":
225                PASSWORD = a
226            elif o in "--source":
227                SOURCE_PROJECT = a
228            elif o in "--target":
229                TARGET_PROJECT = a
230            elif o in "--category":
231                TARGET_CATEGORY = a
232            elif o in "--subject":
233                SUBJECT = a
234        # @@refactor this! all these globals make my head hurt
235        bc = Basecamp("http://%s/" % HOSTNAME, USERNAME, PASSWORD)
236        source_project_id = get_project_id(SOURCE_PROJECT)
237        create_message(get_project_id(TARGET_PROJECT), 
238                       get_target_category_id(TARGET_CATEGORY), 
239                       SUBJECT, 
240                       posts_today())
241
Note: See TracBrowser for help on using the browser.