Skip to content

Instantly share code, notes, and snippets.

@victorzinho
Created November 18, 2015 10:39
Show Gist options
  • Save victorzinho/76a620fc00d2e7cefd60 to your computer and use it in GitHub Desktop.
Save victorzinho/76a620fc00d2e7cefd60 to your computer and use it in GitHub Desktop.
Synchronize Toggl entries with Redmine
#!/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();
@victorzinho
Copy link
Author

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:
{
  "toggl" : {
    "key" : "your_key_here",
    "url" : "https://www.toggl.com/api/v8/time_entries"
  },
  "redmine" : {
    "key" : "your_key_here",
    "url" : "http://<your_server_here>/redmine/time_entries.json"
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment