198 lines
7 KiB
Python
198 lines
7 KiB
Python
#!/bin/python3
|
|
"""
|
|
toggl2sheets is a script that automatically gets time entries from toggl
|
|
and puts them into a google spreadsheet. toggl2sheets will only add *new*
|
|
entries (since it last ran), modified entries will not be updated.
|
|
|
|
A hidden spreadsheet "_toggl2sheets" will be created to keep track when
|
|
was the last execution of this script.
|
|
|
|
PLEASE FILL OUT ALL THE CONSTANTS JUST UNDER ALL THE IMPORT STATEMENTS
|
|
OR PROVIDE APPROPRIATE ENVIRONMENT VARIABLES
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
The script operates based on project names. It will put all entries
|
|
that are a part of project "A" into a worksheet with with the name "A".
|
|
It can create new sheets when the project worksheet does not exist, or just
|
|
ignore it. (IGNORE_PROJECTS_WITHOUT_SHEET option)
|
|
"""
|
|
from collections import defaultdict
|
|
import os
|
|
from typing import Dict, List
|
|
|
|
import datetime
|
|
import pygsheets
|
|
from pygsheets.spreadsheet import Spreadsheet
|
|
from pygsheets.worksheet import Worksheet
|
|
import requests
|
|
|
|
|
|
# ------------------------------
|
|
|
|
# To obtain a Google service account private key, follow instructions
|
|
# here: https://pygsheets.readthedocs.io/en/stable/authorization.html#service-account
|
|
# You will need to:
|
|
# 1. create a new Google Cloud project (https://console.developers.google.com/)
|
|
# 2. go to the credentials tab and make a new service account credential
|
|
# - when creating a new service account, you do not need to add any roles to it
|
|
# 3. go to the created service account and add a key. Click: create new private key.
|
|
# You should now have the key downloaded.
|
|
# 4. point the GOOGLE_SAF variable to that file
|
|
# 5. got to "Enabled APIs & services" in the Google developer console and enable
|
|
# "Google Sheets API"
|
|
GOOGLE_SAF = os.getenv("GOOGLE_SAF", "./service.json")
|
|
|
|
# To get your toggl token go to your profile page (https://track.toggl.com/profile),
|
|
# scroll down, and click reveal on your API key.
|
|
TOGGL_TOKEN = os.getenv("TOGGL_TOKEN", "9e5c9000000000000000000000fd2291")
|
|
|
|
|
|
# To get the workspace id open a report from it in toggl.
|
|
# You can see the ID in the URL
|
|
TOGGL_WORKSPACE = int(os.getenv("TOGGL_WORKSPACE", "42000"))
|
|
|
|
# To get your Google sheet ID, open the sheet, and copy this part of the URL:
|
|
# https://docs.google.com/spreadsheets/d/1Li3djx7xbC00000000000000000iyd2zWv7jjXI_qFA/edit
|
|
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
GOOGLE_SHEET = os.getenv("GOOGLE_SHEET", "1Li3djx7xbC000000v7jjXI_qFA")
|
|
|
|
# Self explainatory. If set to False, then the script will create a new sheet
|
|
IGNORE_PROJECTS_WITHOUT_SHEET = bool(
|
|
int(os.getenv("IGNORE_PROJECTS_WITHOUT_SHEET", "1"))
|
|
)
|
|
|
|
# houry wage
|
|
HOURLY_WAGE = os.getenv("HOURLY_WAGE", "0,00 Kč")
|
|
|
|
# ------------------------------
|
|
|
|
today = datetime.datetime.now().date() - datetime.timedelta(days=1)
|
|
|
|
|
|
def append_hours(worksheet: Worksheet, data: List[List[str]]):
|
|
# add a calculated hours * wage cell to all rows
|
|
# start at number 2 to skip header
|
|
d = [[*x, f"=C{i}*D{i}"] for i, x in enumerate(data, start=2)]
|
|
column = worksheet.get_col(1)
|
|
|
|
# get last non-empty row
|
|
last_row = next(filter(lambda x: not x[1], enumerate(column)))[0]
|
|
|
|
worksheet.insert_rows(last_row, len(d), d)
|
|
|
|
# inserting rows changes the top sums
|
|
worksheet.update_value("C1", f"=SUM(C2:C{last_row+len(d)})", True)
|
|
worksheet.update_value("E1", f"=SUM(E2:E{last_row+len(d)})", True)
|
|
|
|
|
|
def get_toggl_entries(
|
|
token: str, work_id: int, last_edit: datetime.datetime
|
|
) -> Dict[str, List]:
|
|
entries = []
|
|
page = 1
|
|
count = 0
|
|
total_count = 1 # 0 < 1, so we will certainly make at least one request
|
|
while count < total_count:
|
|
resp = requests.get(
|
|
"https://api.track.toggl.com/reports/api/v2/details",
|
|
params={
|
|
"workspace_id": f"{work_id}",
|
|
"user_agent": "autohours",
|
|
"since": last_edit.strftime("%Y-%m-%d"),
|
|
"page": page,
|
|
"order_desc": "off",
|
|
},
|
|
auth=(token, "api_token"),
|
|
)
|
|
if resp.status_code != 200:
|
|
raise ValueError(
|
|
f"Toggl sent a non 200 response: {resp.status_code}, {resp.text}"
|
|
)
|
|
json = resp.json()
|
|
entries.extend(json["data"])
|
|
|
|
total_count = json["total_count"]
|
|
count += json["per_page"]
|
|
page += 1
|
|
|
|
# construct a dict of d["project name"] = [[date, description, duration (hours), wage], ...]
|
|
out = defaultdict(list)
|
|
for entry in entries:
|
|
start = datetime.datetime.fromisoformat(entry["start"])
|
|
end = datetime.datetime.fromisoformat(entry["end"])
|
|
if end < last_edit:
|
|
continue # get better accuracy than toggl lets us in their requests
|
|
out[entry["project"]].append(
|
|
[
|
|
start.strftime("%-d. %-m. %Y"),
|
|
entry["description"],
|
|
round(entry["dur"] / 1000 / 60 / 60, 2),
|
|
HOURLY_WAGE,
|
|
]
|
|
)
|
|
|
|
return out
|
|
|
|
|
|
def get_or_create_last_edit(sh: Spreadsheet) -> datetime.datetime:
|
|
try:
|
|
worksheet: Worksheet = sh.worksheet("title", "_toggl2sheets") # type: ignore
|
|
except pygsheets.WorksheetNotFound:
|
|
time = datetime.datetime.now(datetime.timezone.utc) - datetime.timedelta(days=7)
|
|
worksheet = sh.add_worksheet("_toggl2sheets")
|
|
worksheet.hidden = True
|
|
worksheet.update_value("A1", time.isoformat())
|
|
worksheet.cell("A2").set_text_format("bold", True).value = ( # type: ignore
|
|
"Internal record of when toggl2sheets last "
|
|
"updated the spreadsheet, please do not modify"
|
|
)
|
|
return time
|
|
else:
|
|
val = worksheet.get_value("A1")
|
|
return datetime.datetime.fromisoformat(val)
|
|
|
|
|
|
def update_last_edit(sh: Spreadsheet):
|
|
worksheet: Worksheet = sh.worksheet("title", "_toggl2sheets") # type: ignore
|
|
time = datetime.datetime.now(datetime.timezone.utc)
|
|
worksheet.update_value("A1", time.isoformat())
|
|
|
|
|
|
def main():
|
|
gc = pygsheets.authorize(service_file=GOOGLE_SAF)
|
|
try:
|
|
sh = gc.open_by_key(GOOGLE_SHEET)
|
|
except:
|
|
raise ValueError(
|
|
f"Cannot open google sheet. Do you have permission? Share sheet with \n{gc.oauth.service_account_email}"
|
|
)
|
|
|
|
# get last edit time
|
|
last_edit = get_or_create_last_edit(sh)
|
|
|
|
entries = get_toggl_entries(TOGGL_TOKEN, TOGGL_WORKSPACE, last_edit)
|
|
|
|
for key, val in entries.items():
|
|
try:
|
|
worksheet: Worksheet = sh.worksheet("title", key) # type: ignore
|
|
except pygsheets.WorksheetNotFound:
|
|
if not IGNORE_PROJECTS_WITHOUT_SHEET:
|
|
# create a new worksheet with a header
|
|
worksheet = sh.add_worksheet(key)
|
|
worksheet.insert_rows(
|
|
0, 1, [["Datum", "Popis", "0", "Hodinovka", "0,0 Kč"]]
|
|
)
|
|
else:
|
|
continue
|
|
|
|
append_hours(worksheet, val)
|
|
|
|
update_last_edit(sh)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
main()
|
|
except Exception as e:
|
|
print("ERROR:", str(e))
|
|
exit(1)
|