commit 58c2df8faa8ae575d4e03d22b380268d6c6e81e7
Author: bain <bain@bain.cz>
Date:   Thu Aug 11 23:11:21 2022 +0200

    initial commit

diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 0000000..a20b038
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,2 @@
+pygsheets~=2.0.5
+requests~=2.27.1
diff --git a/toggl2sheets.py b/toggl2sheets.py
new file mode 100644
index 0000000..3d4e2f0
--- /dev/null
+++ b/toggl2sheets.py
@@ -0,0 +1,198 @@
+#!/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)