Skip to content

Commit 05ef71d

Browse files
committed
feat: improve Toggl dedup accuracy, add request timeouts, fix Trakt query params
2 parents 8279270 + 394da2e commit 05ef71d

7 files changed

Lines changed: 21 additions & 21 deletions

File tree

.gitignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,5 @@ logs/
6868
.cache/
6969

7070
# Testing
71-
.tox/
72-
.coverage.*
7371
coverage.xml
7472
*.cover

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ indent-style = "space"
4444

4545
[tool.pytest.ini_options]
4646
testpaths = ["tests"]
47+
pythonpath = ["src"]
4748
python_files = ["test_*.py"]
4849
python_classes = ["Test*"]
4950
python_functions = ["test_*"]

src/toggl.py

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ class TogglAPI:
1313
"""Toggl API client for time tracking."""
1414

1515
BASE_URL = "https://api.track.toggl.com/api/v9"
16+
DEFAULT_TIMEOUT = (3.05, 10)
1617

1718
def __init__(self, api_token, workspace_id, project_id, tags):
1819
self.api_token = api_token
@@ -56,6 +57,7 @@ def get_cached_entries(self, start_date=None, force_refresh=False):
5657
f"{self.BASE_URL}/me/time_entries",
5758
params=params,
5859
auth=(self.api_token, "api_token"),
60+
timeout=self.DEFAULT_TIMEOUT,
5961
)
6062
response.raise_for_status()
6163
self._cached_entries = response.json()
@@ -126,6 +128,7 @@ def create_entry(self, description, start_time, end_time):
126128
f"{self.BASE_URL}/workspaces/{self.workspace_id}/time_entries",
127129
json=data,
128130
auth=(self.api_token, "api_token"),
131+
timeout=self.DEFAULT_TIMEOUT,
129132
)
130133
response.raise_for_status()
131134
start_dt = self.parse_time(start_time).strftime("%Y-%m-%d %H:%M")
@@ -159,6 +162,7 @@ def remove_duplicates(self):
159162
f"{self.BASE_URL}/me/time_entries",
160163
params=params,
161164
auth=(self.api_token, "api_token"),
165+
timeout=self.DEFAULT_TIMEOUT,
162166
)
163167
response.raise_for_status()
164168
except requests.exceptions.HTTPError as e:
@@ -193,32 +197,36 @@ def remove_duplicates(self):
193197

194198
print(f"[{timestamp()}] Found {len(filtered_entries)} Toggl entries in project")
195199

196-
# Find duplicates by description
197-
entries_by_description = {}
200+
# Find duplicates by (description, start, stop)
201+
entries_by_key = {}
198202
for entry in filtered_entries:
199203
desc = entry.get("description", "")
200204
if desc:
201-
if desc not in entries_by_description:
202-
entries_by_description[desc] = []
203-
entries_by_description[desc].append(entry)
205+
start = self.normalize_timestamp(entry["start"]).isoformat()
206+
stop = self.normalize_timestamp(entry["stop"]).isoformat() if entry.get("stop") else ""
207+
key = (desc, start, stop)
208+
if key not in entries_by_key:
209+
entries_by_key[key] = []
210+
entries_by_key[key].append(entry)
204211

205-
duplicates = {desc: entries for desc, entries in entries_by_description.items() if len(entries) > 1}
212+
duplicates = {key: entries for key, entries in entries_by_key.items() if len(entries) > 1}
206213

207214
if duplicates:
208215
total_deleted = 0
209216
entries_to_delete_count = sum(len(entries) - 1 for entries in duplicates.values())
210217
print(f"[{timestamp()}] Found {entries_to_delete_count} duplicate Toggl entries to remove:")
211218

212-
for description, entries in duplicates.items():
213-
print(f" - {description} ({len(entries)} occurrences)")
214-
entries.sort(key=lambda x: x.get("start", ""))
219+
for key, entries in duplicates.items():
220+
print(f" - {key[0]} ({len(entries)} occurrences)")
221+
entries.sort(key=lambda x: x.get("id", 0))
215222
entries_to_delete = entries[:-1]
216223

217224
for entry in entries_to_delete:
218225
start = self.parse_time(entry["start"]).strftime("%Y-%m-%d %H:%M")
219226
response = requests.delete(
220227
f"{self.BASE_URL}/time_entries/{entry['id']}",
221228
auth=(self.api_token, "api_token"),
229+
timeout=self.DEFAULT_TIMEOUT,
222230
)
223231
if response.status_code == 200:
224232
print(f" ✓ Deleted: {start}")

src/trakt.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -140,9 +140,9 @@ def fetch_history(self, access_token, start_date):
140140

141141
while True:
142142
response = requests.get(
143-
f"{self.BASE_URL}/sync/history?extended=full",
143+
f"{self.BASE_URL}/sync/history",
144144
headers=headers,
145-
params={"start_at": start_date, "page": page, "limit": 100},
145+
params={"extended": "full", "start_at": start_date, "page": page, "limit": 100},
146146
timeout=self.DEFAULT_TIMEOUT,
147147
)
148148
response.raise_for_status()

tests/pytest.e2e.ini

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
[pytest]
55
testpaths = tests
6+
pythonpath = src
67
python_files = test_e2e.py
78
python_classes = Test*
89
python_functions = test_*

tests/test_e2e.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,16 +25,12 @@
2525
"""
2626

2727
import os
28-
import sys
2928
import time
3029
from datetime import UTC, datetime, timedelta
3130

3231
import pytest
3332
import requests
3433

35-
# Add src to path
36-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
37-
3834
from toggl import TogglAPI
3935
from trakt import TraktAPI
4036

tests/test_sync.py

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
import json
22
import os
3-
import sys
43
from datetime import datetime, timedelta
54
from unittest.mock import patch
65

76
import pytest
87

9-
# Add parent directory to path to import modules
10-
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "src"))
11-
128
import utils
139
from toggl import TogglAPI
1410
from trakt import TraktAPI

0 commit comments

Comments
 (0)