@@ -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 } " )
0 commit comments