Created
March 20, 2014 20:49
-
-
Save mdaniel/9673503 to your computer and use it in GitHub Desktop.
Using ``htmlize-buffer`` one can convert a clock-ed org file into a series of PivotalTracker chores
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#! /usr/bin/env python | |
# -*- coding: utf-8 -*- | |
from __future__ import print_function, unicode_literals | |
__docformat__ = 'reStructuredText' | |
""" | |
To use this: | |
1. Inside emacs issue ``(require 'htmlize)`` followed by ``M-x htmlize-buffer`` | |
1. Save that buffer to a file | |
1. Run this script on the file | |
1. Profit! | |
""" | |
from datetime import datetime | |
import json | |
import os.path | |
import re | |
import sys | |
import time | |
import requests | |
TOKEN = '' | |
PROJECT_ID = 0 | |
ENDPOINT = 'https://www.pivotaltracker.com' \ | |
'/services/v5/projects/%s' % PROJECT_ID | |
def create_chore(chore_dict): | |
""" | |
:type chore_dict: dict from unicode to object | |
""" | |
headers = { | |
# requests fills in C-Len | |
'Content-Type': 'application/json;charset=UTF-8', | |
'X-TrackerToken': TOKEN, | |
} | |
create_url = '%s/stories' % ENDPOINT | |
# copy for mutation | |
chore = dict(chore_dict) | |
# ensure description ends with a newline | |
chore['description'] = '%s\n' % chore['description'] | |
if 'created_at' not in chore: | |
chore['created_at'] = datetime.utcnow() | |
#: :type: datetime | |
c_datetime = chore['created_at'] | |
created_at_str = re.sub(r'\.\d+$', 'Z', c_datetime.isoformat()) | |
if not created_at_str.endswith('Z'): | |
created_at_str = '%sZ' % created_at_str | |
chore['created_at'] = created_at_str | |
del created_at_str, c_datetime | |
if chore['accepted_at'] is None: | |
chore['accepted_at'] = datetime.utcnow() | |
#: :type: datetime | |
a_datetime = chore['accepted_at'] | |
accepted_at_str = re.sub(r'\.\d+$', 'Z', a_datetime.isoformat()) | |
if not accepted_at_str.endswith('Z'): | |
accepted_at_str = '%sZ' % accepted_at_str | |
chore['accepted_at'] = accepted_at_str | |
del accepted_at_str, a_datetime | |
chore_defaults = { | |
# this comes back in the response but is r/o in the POST | |
# 'kind': 'story', | |
'story_type': 'chore', | |
'estimate': 1, | |
'current_state': 'started', | |
} | |
chore_defaults.update(chore) | |
chore = chore_defaults | |
payload = json.dumps(chore, encoding='utf-8') | |
# print(payload) | |
#: :type: requests.Response | |
res = requests.post(create_url, headers=headers, data=payload) | |
if not res.ok: | |
print('Bogus: %s (%s)\n%s' % | |
(res.status_code, res.reason, res.content), file=sys.stderr) | |
return 1 | |
print('OK!: %s (%s)\n%s\n%s' % | |
(res.status_code, res.reason, res.headers, res.text)) | |
return 0 | |
def convert_time(txt): | |
try: | |
result = datetime.utcfromtimestamp(long(txt)) | |
except ValueError: | |
from pytz import timezone | |
us_pdt = timezone('US/Pacific') | |
#: :type: datetime | |
result = us_pdt.localize(datetime.strptime(txt, '%Y-%m-%d %H:%M:%S')) | |
result = datetime.utcfromtimestamp(time.mktime(result.timetuple())) | |
return result | |
def ingest_file(filename): | |
l2re = re.compile(r'<span class="org-level-2">\*\*\s+([^<]+)</span>') | |
org_date_re = re.compile( | |
r'<span class="org-date">' | |
r'\[(\d{4}-\d{2}-\d{2}) ... (\d\d:\d\d)\]' | |
r'--' | |
r'\[(\d{4}-\d{2}-\d{2}) ... (\d\d:\d\d)\]</span>') | |
org_fixups = [ | |
(re.compile(r'<span class="org-verbatim">=(.+?)=</span>'), r'``\1``'), | |
] | |
def publish_chore(chore_dict): | |
print('== Publish Chore ==\n%r' % chore_dict) | |
create_chore(chore_dict) | |
fh = open(filename) | |
chore = {} | |
for line in fh.readlines(): | |
if 'class="org-level-2' in line: | |
ma = l2re.search(line) | |
if not ma: | |
raise ValueError('Unable to decipher <<%r>>' % line) | |
if chore: | |
publish_chore(chore) | |
chore = { | |
'name': ma.group(1) | |
} | |
elif 'name' not in chore: | |
continue | |
elif 'class="org-level-1' in line: | |
break | |
elif 'org-special-keyword' in line and 'org-date' in line: | |
ma = org_date_re.search(line) | |
if not ma: | |
raise ValueError('Unable to deconstruct org-date\n%r' % line) | |
d0 = ma.group(1) | |
t0 = ma.group(2) | |
d1 = ma.group(3) | |
t1 = ma.group(4) | |
chore['created_at'] = convert_time('%s %s:00' % (d0, t0)) | |
chore['accepted_at'] = convert_time('%s %s:00' % (d1, t1)) | |
chore['current_state'] = 'accepted' | |
else: | |
desc = chore.get('description') | |
line = line.strip() | |
for fix_re, fix_sub in org_fixups: | |
line = fix_re.sub(fix_sub, line) | |
if not desc: | |
chore['description'] = line | |
else: | |
chore['description'] = '%s\n%s' % (desc, line) | |
fh.close() | |
if chore: | |
publish_chore(chore) | |
def main(argv): | |
fn = argv[1] | |
if not os.path.exists(fn): | |
print('Your file "%s" is 404' % fn, file=sys.stderr) | |
sys.exit(1) | |
if not os.path.isfile(fn): | |
print('Expected "%s" to be a file' % fn, file=sys.stderr) | |
sys.exit(1) | |
ingest_file(fn) | |
if __name__ == '__main__': | |
main(sys.argv) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment