Created
November 18, 2015 10:39
-
-
Save victorzinho/76a620fc00d2e7cefd60 to your computer and use it in GitHub Desktop.
Synchronize Toggl entries with Redmine
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 | |
import os, sys, json, requests, datetime, pytz, urllib, re, traceback | |
DIR = os.path.dirname(sys.argv[0]) | |
LAST_UPDATE_FILE = os.path.join(DIR, ".sync_toggl_last_update") | |
LOG_FILE = os.path.join(DIR, "sync_toggl.log") | |
# You can obtain client IDs from wid property in https://www.toggl.com/api/v8/clients | |
CLIENTS = [ <insert_your_client_here> ] | |
# We always use the same activity. | |
# You can take it from any time entry in http://<your_redmine_url>/time_entries.json | |
ACTIVITY_ID = <insert_your_activity_here> | |
# Read custom properties | |
PROPS_FILE = os.path.expanduser("~/.sync_togglrc") | |
with open(PROPS_FILE) as f: | |
props = json.load(f) | |
# Toggl | |
toggl = props["toggl"] | |
if not toggl: | |
raise IOError("Cannot read Toggl API key from " + PROPS_FILE) | |
toggl_key = toggl["key"] | |
if not toggl_key: | |
raise IOError("Cannot read Toggl API key from " + PROPS_FILE) | |
toggl_url = toggl["url"] | |
if not toggl_url: | |
raise IOError("Cannot read Toggl URL from " + PROPS_FILE) | |
# Redmine | |
redmine = props["redmine"] | |
if not redmine: | |
raise IOError("Cannot read Redmine API key from " + PROPS_FILE) | |
redmine_key = redmine["key"] | |
if not redmine_key: | |
raise IOError("Cannot read Redmine API key from " + PROPS_FILE) | |
redmine_url = redmine["url"] | |
if not redmine_url: | |
raise IOError("Cannot read Redmine URL from " + PROPS_FILE) | |
# Build Toggl query from last update, if any | |
now = datetime.datetime.utcnow().replace(tzinfo = pytz.utc) | |
now_iso = now.isoformat() | |
if os.path.isfile(LAST_UPDATE_FILE): | |
f = open(LAST_UPDATE_FILE, 'r') | |
last_update = f.read().strip() | |
end = now + datetime.timedelta(days=1) | |
url_query = "?start_date=" + urllib.quote(last_update) + "&end_date=" + urllib.quote(end.isoformat()) | |
f.close() | |
else: | |
raise IOError("Cannot find " + LAST_UPDATE_FILE + " file.\n" + | |
"Please create it with the time you want to start synchronizing, " + | |
"using the following example:\n2015-11-11T12:00:00+00:00") | |
# Obtain time entries | |
response = requests.get(toggl_url + url_query, auth=(toggl_key, 'api_token')) | |
if response.status_code != 200: | |
raise IOError("Request failed with code " + str(response.status_code)) | |
log = open(LOG_FILE, 'a') | |
for entry in response.json(): | |
if "description" not in entry: | |
log.write("[" + now_iso + "] Ignoring time entry without description.") | |
continue | |
try: | |
issues = re.findall("#[0-9]{1,6}", entry["description"]) | |
if len(issues) != 1: | |
log.write("[" + now_iso + "] Ignoring time entry with description '" | |
+ entry["description"] + "'. Description is not a valid issue identifier.\n") | |
continue | |
if entry["wid"] not in CLIENTS: | |
log.write("[" + now_iso + "] Ignoring time entry with description '" | |
+ entry["description"] + "'. Client is not known.\n") | |
continue | |
if "stop" not in entry: | |
log.write("[" + now_iso + "] Ignoring time entry with description '" | |
+ entry["description"] + "'. Entry has not finished.\n") | |
continue | |
hours = int(entry["duration"]) / 3600.0 | |
issue = int(issues[0][1:]) | |
spent_on = entry["start"].split("T")[0] | |
data = { "time_entry": { "spent_on": spent_on, "hours": hours, "issue_id": issue, "activity_id": ACTIVITY_ID } } | |
headers = { "Content-Type" : "application/json", "X-Redmine-API-Key" : redmine_key } | |
print "Importing " + entry["description"] | |
response = requests.post(redmine_url, data=json.dumps(data), headers=headers) | |
if response.status_code != 201: | |
print "Failed to POST entry " + idstring | |
except Exception as e: | |
log.write("[" + now_iso + "] Ignoring time entry with description '" | |
+ entry["description"] + "'." + traceback.format_exc(e) + ".\n") | |
log.close() | |
# Update last_update file | |
f = open(LAST_UPDATE_FILE,'w') | |
f.write(now_iso) | |
f.close(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Quite ad-hoc solution. It takes the Toggl entries that have an issue identifier (
#<number>
) in the description and belong to a specific set of clients (be sure to configure them on the top of the script); it adds them to Redmine using a constant activity ID (be sure to configure that as well).You need to create the following files before start using it:
.sync_toggl_last_update
in the same directory as the script with the timestamp you want to start synchronizing, such as:2015-11-18T10:25:57.516496+00:00
..sync_togglrc
in your home directory with the Toggl and Redmine URLs and API keys, such as: