Skip to content

Trackers

toggl_api.TrackerBody dataclass

Bases: BaseBody

JSON body dataclass for PUT, POST & PATCH requests.

Examples:

>>> TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
METHOD DESCRIPTION
format

Format the body for JSON requests.

format

format(endpoint: str, **body: Any) -> dict[str, Any]

Format the body for JSON requests.

Gets called by the endpoint methods before requesting.

PARAMETER DESCRIPTION
endpoint

The endpoints name for filtering purposes.

TYPE: str

body

Additional body arguments that the endpoint requires.

TYPE: Any DEFAULT: {}

RETURNS DESCRIPTION
dict[str, Any]

JSON compatible formatted body.

toggl_api.TrackerEndpoint

Bases: TogglCachedEndpoint[TogglTracker]

Endpoint for modifying and creating trackers.

Official Documentation

Examples:

>>> tracker_endpoint = TrackerEndpoint(324525, BasicAuth(...), JSONCache(Path("cache")))
>>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
>>> tracker_endpoint.add(body)
TogglTracker(id=58687689, name="What a wonderful tracker description!", project=2123132, ...)
>>> tracker_endpoint.delete(tracker)
None
PARAMETER DESCRIPTION
workspace_id

The workspace the Toggl trackers belong to.

TYPE: int | TogglWorkspace

auth

Authentication for the client.

TYPE: BasicAuth

cache

Where to cache trackers.

TYPE: TogglCache[TogglTracker] | None DEFAULT: None

client

Optional client to be passed to be used for requests. Useful when a global client is used and needs to be recycled.

TYPE: Client | None DEFAULT: None

timeout

How long it takes for the client to timeout. Keyword Only. Defaults to 10 seconds.

TYPE: Timeout | int DEFAULT: 10

re_raise

Whether to raise all HTTPStatusError errors and not handle them internally. Keyword Only.

TYPE: bool DEFAULT: False

retries

Max retries to attempt if the server returns a 5xx status_code. Has no effect if re_raise is True. Keyword Only.

TYPE: int DEFAULT: 3

METHOD DESCRIPTION
current

Get current running tracker. Returns None if no tracker is running.

collect

Get a set of trackers depending on specified parameters.

get

Get a single tracker by ID.

add

Add a new tracker.

edit

Edit an existing tracker based on the supplied parameters within the body.

bulk_edit

Bulk edit multiple trackers at the same time.

delete

Delete a tracker from Toggl.

stop

Stop the currently running tracker.

Source code in src/toggl_api/_tracker.py
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
class TrackerEndpoint(TogglCachedEndpoint[TogglTracker]):
    """Endpoint for modifying and creating trackers.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries)

    Examples:
        >>> tracker_endpoint = TrackerEndpoint(324525, BasicAuth(...), JSONCache(Path("cache")))

        >>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
        >>> tracker_endpoint.add(body)
        TogglTracker(id=58687689, name="What a wonderful tracker description!", project=2123132, ...)

        >>> tracker_endpoint.delete(tracker)
        None

    Params:
        workspace_id: The workspace the Toggl trackers belong to.
        auth: Authentication for the client.
        cache: Where to cache trackers.
        client: Optional client to be passed to be used for requests. Useful
            when a global client is used and needs to be recycled.
        timeout: How long it takes for the client to timeout. Keyword Only.
            Defaults to 10 seconds.
        re_raise: Whether to raise all HTTPStatusError errors and not handle them
            internally. Keyword Only.
        retries: Max retries to attempt if the server returns a *5xx* status_code.
            Has no effect if re_raise is `True`. Keyword Only.
    """

    MODEL = TogglTracker
    TRACKER_ALREADY_STOPPED: Final[int] = codes.CONFLICT
    TRACKER_NOT_RUNNING: Final[int] = codes.METHOD_NOT_ALLOWED

    def __init__(
        self,
        workspace_id: int | TogglWorkspace,
        auth: BasicAuth,
        cache: TogglCache[TogglTracker] | None = None,
        *,
        client: Client | None = None,
        timeout: Timeout | int = 10,
        re_raise: bool = False,
        retries: int = 3,
    ) -> None:
        super().__init__(
            auth,
            cache,
            client=client,
            timeout=timeout,
            re_raise=re_raise,
            retries=retries,
        )
        self.workspace_id = workspace_id if isinstance(workspace_id, int) else workspace_id.id

    def _current_refresh(self, tracker: TogglTracker | None) -> None:
        if self.cache and tracker is None:
            try:
                for t in self.cache.query(TogglQuery("stop", None)):
                    self.get(t, refresh=True)
            except HTTPStatusError:
                log.exception("%s")
                return

    def current(self, *, refresh: bool = True) -> TogglTracker | None:
        """Get current running tracker. Returns None if no tracker is running.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-get-current-time-entry)

        Examples:
            >>> tracker_endpoint.current()
            None

            >>> tracker_endpoint.current(refresh=True)
            TogglTracker(...)

        Args:
            refresh: Whether to check the remote API for running trackers.
                If 'refresh' is True it will check if there are any other running
                trackers and update if the 'stop' attribute is None.

        Raises:
            HTTPStatusError: If the request is not a success or any error that's
                not a '405' status code.

        Returns:
            A model from cache or the API. None if nothing is running.
        """
        if self.cache and not refresh:
            query = list(self.cache.query(TogglQuery("stop", None)))
            return query[0] if query else None

        try:
            response = self.request("me/time_entries/current", refresh=refresh)
        except HTTPStatusError as err:
            if not self.re_raise and err.response.status_code == self.TRACKER_NOT_RUNNING:
                log.warning("No tracker is currently running!")
                response = None
            else:
                raise

        self._current_refresh(cast("TogglTracker | None", response))

        return response if isinstance(response, TogglTracker) else None

    def _collect_cache(
        self,
        since: int | datetime | None = None,
        before: date | None = None,
        start_date: date | None = None,
        end_date: date | None = None,
    ) -> list[TogglTracker]:
        cache: list[TogglTracker] = []
        if since or before:
            queries: list[TogglQuery[date]] = []

            if since:
                since = datetime.fromtimestamp(since, tz=timezone.utc) if isinstance(since, int) else since
                queries.append(
                    TogglQuery("timestamp", since, Comparison.GREATER_THEN),
                )

            if before:
                queries.append(
                    TogglQuery("start", before, Comparison.LESS_THEN),
                )

            cache.extend(self.query(*queries))

        elif start_date and end_date:
            cache.extend(
                self.query(
                    TogglQuery(
                        "start",
                        start_date,
                        Comparison.GREATER_THEN_OR_EQUAL,
                    ),
                    TogglQuery(
                        "start",
                        end_date,
                        Comparison.LESS_THEN_OR_EQUAL,
                    ),
                ),
            )
        else:
            cache.extend(self.load_cache())

        return cache

    def collect(
        self,
        since: int | datetime | None = None,
        before: date | None = None,
        start_date: date | None = None,
        end_date: date | None = None,
        *,
        refresh: bool = False,
    ) -> list[TogglTracker]:
        """Get a set of trackers depending on specified parameters.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-timeentries)

        Missing meta and include_sharing query flags not supported by wrapper at
        the moment.

        Examples:
            >>> collect(since=17300032362, before=date(2024, 11, 27))

            >>> collect(refresh=True)

            >>> collect(start_date=date(2024, 11, 27), end_date=date(2024, 12, 27))

        Args:
            since: Get entries modified since this date using UNIX timestamp.
                Includes deleted ones if refreshing.
            before: Get entries with start time, before given date (YYYY-MM-DD)
                or with time in RFC3339 format.
            start_date: Get entries with start time, from start_date YYYY-MM-DD
                or with time in RFC3339 format. To be used with end_date.
            end_date: Get entries with start time, until end_date YYYY-MM-DD or
                with time in RFC3339 format. To be used with start_date.
            refresh: Whether to refresh the cache or not.

        Raises:
            DateTimeError: If the dates are not in the correct ranges.
            HTTPStatusError: If the request is not a successful status code.

        Returns:
           List of TogglTracker objects that are within specified parameters.
                Empty if none is matched.
        """
        if start_date and end_date:
            if end_date < start_date:
                msg = "end_date must be after the start_date!"
                raise DateTimeError(msg)
            if start_date > datetime.now(tz=timezone.utc):
                msg = "start_date must not be earlier than the current date!"
                raise DateTimeError(msg)

        if not refresh:
            return self._collect_cache(since, before, start_date, end_date)

        params = "me/time_entries"
        if since or before:
            if since:
                params += f"?since={get_timestamp(since)}"

            if before:
                params += "&" if since else "?"
                params += f"before={format_iso(before)}"

        elif start_date and end_date:
            params += f"?start_date={format_iso(start_date)}&end_date={format_iso(end_date)}"

        response = self.request(params, refresh=refresh)

        return response if isinstance(response, list) else []

    def get(
        self,
        tracker_id: int | TogglTracker,
        *,
        refresh: bool = False,
    ) -> TogglTracker | None:
        """Get a single tracker by ID.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-get-a-time-entry-by-id)

        Args:
            tracker_id: ID of the tracker to get.
            refresh: Whether to refresh the cache or not.

        Raises:
            HTTPStatusError: If anything thats not a *ok* or *404* status code
                is returned.

        Returns:
            TogglTracker object or None if not found.
        """
        if isinstance(tracker_id, TogglTracker):
            tracker_id = tracker_id.id

        if self.cache and not refresh:
            return self.cache.find({"id": tracker_id})

        try:
            response = self.request(
                f"me/time_entries/{tracker_id}",
                refresh=refresh,
            )
        except HTTPStatusError as err:
            if not self.re_raise and err.response.status_code == codes.NOT_FOUND:
                log.warning("Tracker with id %s does not exist!", tracker_id)
                return None
            raise

        return cast("TogglTracker", response)

    def edit(
        self,
        tracker: TogglTracker | int,
        body: TrackerBody,
        *,
        meta: bool = False,
    ) -> TogglTracker:
        """Edit an existing tracker based on the supplied parameters within the body.

        This endpoint always hit the external API in order to keep trackers consistent.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#put-timeentries)

        Examples:
            >>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
            >>> tracker_endpoint.edit(58687684, body)
            TogglTracker(id=58687684, name="What a wonderful tracker description!", project=2123132, ...)

        Args:
            tracker: Target tracker model or id to edit.
            body: Updated content to add.
            meta: Should the response contain data for meta entities.

        Raises:
            HTTPStatusError: For anything thats not a *ok* status code.

        Returns:
            A new model if successful else None.
        """
        if (body.tag_ids or body.tags) and not body.tag_action:
            body.tag_action = "add"

        if isinstance(tracker, TogglTracker):
            tracker = tracker.id

        return cast(
            "TogglTracker",
            self.request(
                f"{self.endpoint}/{tracker}",
                method=RequestMethod.PUT,
                body=body.format(
                    "edit",
                    workspace_id=self.workspace_id,
                    meta=meta,
                ),
                refresh=True,
            ),
        )

    def _bulk_edit(
        self,
        trackers: list[int],
        body: list[BulkEditParameter],
    ) -> dict[str, list[int]]:
        return cast(
            "dict[str, list[int]]",
            cast(
                "Response",
                self.request(
                    f"{self.endpoint}/" + ",".join([str(t) for t in trackers]),
                    body=body,
                    refresh=True,
                    method=RequestMethod.PATCH,
                    raw=True,
                ),
            ).json(),
        )

    def bulk_edit(
        self,
        *trackers: int | TogglTracker,
        body: TrackerBody,
    ) -> Edits:
        """Bulk edit multiple trackers at the same time.

        Patch will be executed partially when there are errors with some records.
        No transaction, no rollback.

        There is a limit of editing 100 trackers at the same time, so the
        method will make multiple calls if the count exceeds that limit.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries/#patch-bulk-editing-time-entries)

        Examples:
            >>> body = TrackerBody(description="All these trackers belong to me!")
            >>> tracker_endpoint.bulk_edit(1235151, 214124, body)
            Edits(successes=[1235151, 214124], failures=[])

        Args:
            trackers: All trackers that need to be edited.
            body: The parameters that need to be edited.

        Raises:
            HTTPStatusError: For anything thats not a *ok* status code.

        Returns:
            Successeful or failed ids editing the trackers.
        """
        tracker_ids = [t if isinstance(t, int) else t.id for t in trackers]
        requests = math.ceil(len(tracker_ids) / 100)
        success: list[int]
        failure: list[int]
        success, failure = [], []

        fmt_body = body._format_bulk_edit()  # noqa: SLF001
        for i in range(requests):
            edit = self._bulk_edit(
                tracker_ids[100 * i : 100 + (100 * i)],
                fmt_body,
            )
            success.extend(edit["success"])
            failure.extend(edit["failure"])

        return Edits(success, failure)

    def delete(self, tracker: TogglTracker | int) -> None:
        """Delete a tracker from Toggl.

        This endpoint always hit the external API in order to keep trackers consistent.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#delete-timeentries)

        Examples:
            >>> tracker_endpoint.delete(58687684)
            None

        Args:
            tracker: Tracker object with ID to delete.

        Raises:
            HTTPStatusError: If anything thats not a '404' or 'ok' code is returned.

        """
        tracker_id = tracker if isinstance(tracker, int) else tracker.id
        try:
            self.request(
                f"{self.endpoint}/{tracker_id}",
                method=RequestMethod.DELETE,
                refresh=True,
            )
        except HTTPStatusError as err:
            if self.re_raise or err.response.status_code != codes.NOT_FOUND:
                raise
            log.warning(
                "Tracker with id %s was either already deleted or did not exist in the first place!",
                tracker_id,
            )
        if self.cache is None:
            return

        if isinstance(tracker, int):
            trk = self.cache.find({"id": tracker})
            if not isinstance(trk, TogglTracker):
                return
            tracker = trk

        self.cache.delete(tracker)
        self.cache.commit()

    def stop(self, tracker: TogglTracker | int) -> TogglTracker | None:
        """Stop the currently running tracker.

        This endpoint always hit the external API in order to keep trackers consistent.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#patch-stop-timeentry)

        Examples:
            >>> tracker_endpoint.stop(58687684)
            TogglTracker(id=58687684, name="What a wonderful tracker description!", ...)

        Args:
            tracker: Tracker id to stop. An integer or model.

        Raises:
            HTTPStatusError: For anything thats not 'ok' or a '409' status code.

        Returns:
           If the tracker was stopped or if the tracker wasn't running it will return None.
        """
        if isinstance(tracker, TogglTracker):
            tracker = tracker.id
        try:
            return cast(
                "TogglTracker",
                self.request(
                    f"{self.endpoint}/{tracker}/stop",
                    method=RequestMethod.PATCH,
                    refresh=True,
                ),
            )
        except HTTPStatusError as err:
            if self.re_raise or err.response.status_code != self.TRACKER_ALREADY_STOPPED:
                raise
            log.warning("Tracker with id %s was already stopped!", tracker)
        return None

    def add(self, body: TrackerBody) -> TogglTracker:
        """Add a new tracker.

        This endpoint always hit the external API in order to keep trackers consistent.

        [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#post-timeentries)

        Examples:
            >>> body = TrackerBody(description="Tracker description!", project_id=2123132)
            >>> tracker_endpoint.edit(body)
            TogglTracker(id=78895400, name="Tracker description!", project=2123132, ...)

        Args:
            body: Body of the request. Description must be set. If start date
                is not set it will be set to current time with duration set
                to -1 for a running tracker.

        Raises:
            HTTPStatusError: For anything that wasn't an *ok* status code.
            NamingError: Description must be set in order to create a new tracker.

        Returns:
            The tracker that was created.
        """
        if not body.description:
            msg = "Description must be set in order to create a tracker!"
            raise NamingError(msg)

        if body.start is None:
            body.start = datetime.now(tz=timezone.utc)
            log.info(
                "Body is missing a start. Setting to %s...",
                body.start,
                extra={"body": body},
            )
            if body.stop is None:
                body.duration = -1

        body.tag_action = "add"

        return cast(
            "TogglTracker",
            self.request(
                self.endpoint,
                method=RequestMethod.POST,
                body=body.format("add", workspace_id=self.workspace_id),
                refresh=True,
            ),
        )

    @property
    def endpoint(self) -> str:
        return f"workspaces/{self.workspace_id}/time_entries"

current

current(*, refresh: bool = True) -> TogglTracker | None

Get current running tracker. Returns None if no tracker is running.

Official Documentation

Examples:

>>> tracker_endpoint.current()
None
>>> tracker_endpoint.current(refresh=True)
TogglTracker(...)
PARAMETER DESCRIPTION
refresh

Whether to check the remote API for running trackers. If 'refresh' is True it will check if there are any other running trackers and update if the 'stop' attribute is None.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
HTTPStatusError

If the request is not a success or any error that's not a '405' status code.

RETURNS DESCRIPTION
TogglTracker | None

A model from cache or the API. None if nothing is running.

Source code in src/toggl_api/_tracker.py
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
def current(self, *, refresh: bool = True) -> TogglTracker | None:
    """Get current running tracker. Returns None if no tracker is running.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-get-current-time-entry)

    Examples:
        >>> tracker_endpoint.current()
        None

        >>> tracker_endpoint.current(refresh=True)
        TogglTracker(...)

    Args:
        refresh: Whether to check the remote API for running trackers.
            If 'refresh' is True it will check if there are any other running
            trackers and update if the 'stop' attribute is None.

    Raises:
        HTTPStatusError: If the request is not a success or any error that's
            not a '405' status code.

    Returns:
        A model from cache or the API. None if nothing is running.
    """
    if self.cache and not refresh:
        query = list(self.cache.query(TogglQuery("stop", None)))
        return query[0] if query else None

    try:
        response = self.request("me/time_entries/current", refresh=refresh)
    except HTTPStatusError as err:
        if not self.re_raise and err.response.status_code == self.TRACKER_NOT_RUNNING:
            log.warning("No tracker is currently running!")
            response = None
        else:
            raise

    self._current_refresh(cast("TogglTracker | None", response))

    return response if isinstance(response, TogglTracker) else None

collect

collect(
    since: int | datetime | None = None,
    before: date | None = None,
    start_date: date | None = None,
    end_date: date | None = None,
    *,
    refresh: bool = False,
) -> list[TogglTracker]

Get a set of trackers depending on specified parameters.

Official Documentation

Missing meta and include_sharing query flags not supported by wrapper at the moment.

Examples:

>>> collect(since=17300032362, before=date(2024, 11, 27))
>>> collect(refresh=True)
>>> collect(start_date=date(2024, 11, 27), end_date=date(2024, 12, 27))
PARAMETER DESCRIPTION
since

Get entries modified since this date using UNIX timestamp. Includes deleted ones if refreshing.

TYPE: int | datetime | None DEFAULT: None

before

Get entries with start time, before given date (YYYY-MM-DD) or with time in RFC3339 format.

TYPE: date | None DEFAULT: None

start_date

Get entries with start time, from start_date YYYY-MM-DD or with time in RFC3339 format. To be used with end_date.

TYPE: date | None DEFAULT: None

end_date

Get entries with start time, until end_date YYYY-MM-DD or with time in RFC3339 format. To be used with start_date.

TYPE: date | None DEFAULT: None

refresh

Whether to refresh the cache or not.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
DateTimeError

If the dates are not in the correct ranges.

HTTPStatusError

If the request is not a successful status code.

RETURNS DESCRIPTION
list[TogglTracker]

List of TogglTracker objects that are within specified parameters. Empty if none is matched.

Source code in src/toggl_api/_tracker.py
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
def collect(
    self,
    since: int | datetime | None = None,
    before: date | None = None,
    start_date: date | None = None,
    end_date: date | None = None,
    *,
    refresh: bool = False,
) -> list[TogglTracker]:
    """Get a set of trackers depending on specified parameters.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-timeentries)

    Missing meta and include_sharing query flags not supported by wrapper at
    the moment.

    Examples:
        >>> collect(since=17300032362, before=date(2024, 11, 27))

        >>> collect(refresh=True)

        >>> collect(start_date=date(2024, 11, 27), end_date=date(2024, 12, 27))

    Args:
        since: Get entries modified since this date using UNIX timestamp.
            Includes deleted ones if refreshing.
        before: Get entries with start time, before given date (YYYY-MM-DD)
            or with time in RFC3339 format.
        start_date: Get entries with start time, from start_date YYYY-MM-DD
            or with time in RFC3339 format. To be used with end_date.
        end_date: Get entries with start time, until end_date YYYY-MM-DD or
            with time in RFC3339 format. To be used with start_date.
        refresh: Whether to refresh the cache or not.

    Raises:
        DateTimeError: If the dates are not in the correct ranges.
        HTTPStatusError: If the request is not a successful status code.

    Returns:
       List of TogglTracker objects that are within specified parameters.
            Empty if none is matched.
    """
    if start_date and end_date:
        if end_date < start_date:
            msg = "end_date must be after the start_date!"
            raise DateTimeError(msg)
        if start_date > datetime.now(tz=timezone.utc):
            msg = "start_date must not be earlier than the current date!"
            raise DateTimeError(msg)

    if not refresh:
        return self._collect_cache(since, before, start_date, end_date)

    params = "me/time_entries"
    if since or before:
        if since:
            params += f"?since={get_timestamp(since)}"

        if before:
            params += "&" if since else "?"
            params += f"before={format_iso(before)}"

    elif start_date and end_date:
        params += f"?start_date={format_iso(start_date)}&end_date={format_iso(end_date)}"

    response = self.request(params, refresh=refresh)

    return response if isinstance(response, list) else []

get

get(
    tracker_id: int | TogglTracker, *, refresh: bool = False
) -> TogglTracker | None

Get a single tracker by ID.

Official Documentation

PARAMETER DESCRIPTION
tracker_id

ID of the tracker to get.

TYPE: int | TogglTracker

refresh

Whether to refresh the cache or not.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
HTTPStatusError

If anything thats not a ok or 404 status code is returned.

RETURNS DESCRIPTION
TogglTracker | None

TogglTracker object or None if not found.

Source code in src/toggl_api/_tracker.py
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
def get(
    self,
    tracker_id: int | TogglTracker,
    *,
    refresh: bool = False,
) -> TogglTracker | None:
    """Get a single tracker by ID.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#get-get-a-time-entry-by-id)

    Args:
        tracker_id: ID of the tracker to get.
        refresh: Whether to refresh the cache or not.

    Raises:
        HTTPStatusError: If anything thats not a *ok* or *404* status code
            is returned.

    Returns:
        TogglTracker object or None if not found.
    """
    if isinstance(tracker_id, TogglTracker):
        tracker_id = tracker_id.id

    if self.cache and not refresh:
        return self.cache.find({"id": tracker_id})

    try:
        response = self.request(
            f"me/time_entries/{tracker_id}",
            refresh=refresh,
        )
    except HTTPStatusError as err:
        if not self.re_raise and err.response.status_code == codes.NOT_FOUND:
            log.warning("Tracker with id %s does not exist!", tracker_id)
            return None
        raise

    return cast("TogglTracker", response)

add

add(body: TrackerBody) -> TogglTracker

Add a new tracker.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> body = TrackerBody(description="Tracker description!", project_id=2123132)
>>> tracker_endpoint.edit(body)
TogglTracker(id=78895400, name="Tracker description!", project=2123132, ...)
PARAMETER DESCRIPTION
body

Body of the request. Description must be set. If start date is not set it will be set to current time with duration set to -1 for a running tracker.

TYPE: TrackerBody

RAISES DESCRIPTION
HTTPStatusError

For anything that wasn't an ok status code.

NamingError

Description must be set in order to create a new tracker.

RETURNS DESCRIPTION
TogglTracker

The tracker that was created.

Source code in src/toggl_api/_tracker.py
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
def add(self, body: TrackerBody) -> TogglTracker:
    """Add a new tracker.

    This endpoint always hit the external API in order to keep trackers consistent.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#post-timeentries)

    Examples:
        >>> body = TrackerBody(description="Tracker description!", project_id=2123132)
        >>> tracker_endpoint.edit(body)
        TogglTracker(id=78895400, name="Tracker description!", project=2123132, ...)

    Args:
        body: Body of the request. Description must be set. If start date
            is not set it will be set to current time with duration set
            to -1 for a running tracker.

    Raises:
        HTTPStatusError: For anything that wasn't an *ok* status code.
        NamingError: Description must be set in order to create a new tracker.

    Returns:
        The tracker that was created.
    """
    if not body.description:
        msg = "Description must be set in order to create a tracker!"
        raise NamingError(msg)

    if body.start is None:
        body.start = datetime.now(tz=timezone.utc)
        log.info(
            "Body is missing a start. Setting to %s...",
            body.start,
            extra={"body": body},
        )
        if body.stop is None:
            body.duration = -1

    body.tag_action = "add"

    return cast(
        "TogglTracker",
        self.request(
            self.endpoint,
            method=RequestMethod.POST,
            body=body.format("add", workspace_id=self.workspace_id),
            refresh=True,
        ),
    )

edit

edit(
    tracker: TogglTracker | int, body: TrackerBody, *, meta: bool = False
) -> TogglTracker

Edit an existing tracker based on the supplied parameters within the body.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
>>> tracker_endpoint.edit(58687684, body)
TogglTracker(id=58687684, name="What a wonderful tracker description!", project=2123132, ...)
PARAMETER DESCRIPTION
tracker

Target tracker model or id to edit.

TYPE: TogglTracker | int

body

Updated content to add.

TYPE: TrackerBody

meta

Should the response contain data for meta entities.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
HTTPStatusError

For anything thats not a ok status code.

RETURNS DESCRIPTION
TogglTracker

A new model if successful else None.

Source code in src/toggl_api/_tracker.py
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
def edit(
    self,
    tracker: TogglTracker | int,
    body: TrackerBody,
    *,
    meta: bool = False,
) -> TogglTracker:
    """Edit an existing tracker based on the supplied parameters within the body.

    This endpoint always hit the external API in order to keep trackers consistent.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#put-timeentries)

    Examples:
        >>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
        >>> tracker_endpoint.edit(58687684, body)
        TogglTracker(id=58687684, name="What a wonderful tracker description!", project=2123132, ...)

    Args:
        tracker: Target tracker model or id to edit.
        body: Updated content to add.
        meta: Should the response contain data for meta entities.

    Raises:
        HTTPStatusError: For anything thats not a *ok* status code.

    Returns:
        A new model if successful else None.
    """
    if (body.tag_ids or body.tags) and not body.tag_action:
        body.tag_action = "add"

    if isinstance(tracker, TogglTracker):
        tracker = tracker.id

    return cast(
        "TogglTracker",
        self.request(
            f"{self.endpoint}/{tracker}",
            method=RequestMethod.PUT,
            body=body.format(
                "edit",
                workspace_id=self.workspace_id,
                meta=meta,
            ),
            refresh=True,
        ),
    )

bulk_edit

bulk_edit(*trackers: int | TogglTracker, body: TrackerBody) -> Edits

Bulk edit multiple trackers at the same time.

Patch will be executed partially when there are errors with some records. No transaction, no rollback.

There is a limit of editing 100 trackers at the same time, so the method will make multiple calls if the count exceeds that limit.

Official Documentation

Examples:

>>> body = TrackerBody(description="All these trackers belong to me!")
>>> tracker_endpoint.bulk_edit(1235151, 214124, body)
Edits(successes=[1235151, 214124], failures=[])
PARAMETER DESCRIPTION
trackers

All trackers that need to be edited.

TYPE: int | TogglTracker DEFAULT: ()

body

The parameters that need to be edited.

TYPE: TrackerBody

RAISES DESCRIPTION
HTTPStatusError

For anything thats not a ok status code.

RETURNS DESCRIPTION
Edits

Successeful or failed ids editing the trackers.

Source code in src/toggl_api/_tracker.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
def bulk_edit(
    self,
    *trackers: int | TogglTracker,
    body: TrackerBody,
) -> Edits:
    """Bulk edit multiple trackers at the same time.

    Patch will be executed partially when there are errors with some records.
    No transaction, no rollback.

    There is a limit of editing 100 trackers at the same time, so the
    method will make multiple calls if the count exceeds that limit.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries/#patch-bulk-editing-time-entries)

    Examples:
        >>> body = TrackerBody(description="All these trackers belong to me!")
        >>> tracker_endpoint.bulk_edit(1235151, 214124, body)
        Edits(successes=[1235151, 214124], failures=[])

    Args:
        trackers: All trackers that need to be edited.
        body: The parameters that need to be edited.

    Raises:
        HTTPStatusError: For anything thats not a *ok* status code.

    Returns:
        Successeful or failed ids editing the trackers.
    """
    tracker_ids = [t if isinstance(t, int) else t.id for t in trackers]
    requests = math.ceil(len(tracker_ids) / 100)
    success: list[int]
    failure: list[int]
    success, failure = [], []

    fmt_body = body._format_bulk_edit()  # noqa: SLF001
    for i in range(requests):
        edit = self._bulk_edit(
            tracker_ids[100 * i : 100 + (100 * i)],
            fmt_body,
        )
        success.extend(edit["success"])
        failure.extend(edit["failure"])

    return Edits(success, failure)

delete

delete(tracker: TogglTracker | int) -> None

Delete a tracker from Toggl.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> tracker_endpoint.delete(58687684)
None
PARAMETER DESCRIPTION
tracker

Tracker object with ID to delete.

TYPE: TogglTracker | int

RAISES DESCRIPTION
HTTPStatusError

If anything thats not a '404' or 'ok' code is returned.

Source code in src/toggl_api/_tracker.py
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
def delete(self, tracker: TogglTracker | int) -> None:
    """Delete a tracker from Toggl.

    This endpoint always hit the external API in order to keep trackers consistent.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#delete-timeentries)

    Examples:
        >>> tracker_endpoint.delete(58687684)
        None

    Args:
        tracker: Tracker object with ID to delete.

    Raises:
        HTTPStatusError: If anything thats not a '404' or 'ok' code is returned.

    """
    tracker_id = tracker if isinstance(tracker, int) else tracker.id
    try:
        self.request(
            f"{self.endpoint}/{tracker_id}",
            method=RequestMethod.DELETE,
            refresh=True,
        )
    except HTTPStatusError as err:
        if self.re_raise or err.response.status_code != codes.NOT_FOUND:
            raise
        log.warning(
            "Tracker with id %s was either already deleted or did not exist in the first place!",
            tracker_id,
        )
    if self.cache is None:
        return

    if isinstance(tracker, int):
        trk = self.cache.find({"id": tracker})
        if not isinstance(trk, TogglTracker):
            return
        tracker = trk

    self.cache.delete(tracker)
    self.cache.commit()

stop

stop(tracker: TogglTracker | int) -> TogglTracker | None

Stop the currently running tracker.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> tracker_endpoint.stop(58687684)
TogglTracker(id=58687684, name="What a wonderful tracker description!", ...)
PARAMETER DESCRIPTION
tracker

Tracker id to stop. An integer or model.

TYPE: TogglTracker | int

RAISES DESCRIPTION
HTTPStatusError

For anything thats not 'ok' or a '409' status code.

RETURNS DESCRIPTION
TogglTracker | None

If the tracker was stopped or if the tracker wasn't running it will return None.

Source code in src/toggl_api/_tracker.py
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
def stop(self, tracker: TogglTracker | int) -> TogglTracker | None:
    """Stop the currently running tracker.

    This endpoint always hit the external API in order to keep trackers consistent.

    [Official Documentation](https://engineering.toggl.com/docs/api/time_entries#patch-stop-timeentry)

    Examples:
        >>> tracker_endpoint.stop(58687684)
        TogglTracker(id=58687684, name="What a wonderful tracker description!", ...)

    Args:
        tracker: Tracker id to stop. An integer or model.

    Raises:
        HTTPStatusError: For anything thats not 'ok' or a '409' status code.

    Returns:
       If the tracker was stopped or if the tracker wasn't running it will return None.
    """
    if isinstance(tracker, TogglTracker):
        tracker = tracker.id
    try:
        return cast(
            "TogglTracker",
            self.request(
                f"{self.endpoint}/{tracker}/stop",
                method=RequestMethod.PATCH,
                refresh=True,
            ),
        )
    except HTTPStatusError as err:
        if self.re_raise or err.response.status_code != self.TRACKER_ALREADY_STOPPED:
            raise
        log.warning("Tracker with id %s was already stopped!", tracker)
    return None

toggl_api.asyncio.AsyncTrackerEndpoint

Bases: TogglAsyncCachedEndpoint[TogglTracker]

Endpoint for modifying and creating trackers.

Official Documentation

Examples:

>>> tracker_endpoint = TrackerEndpoint(324525, BasicAuth(...), AsyncSqliteCache(Path("cache")))
>>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
>>> await tracker_endpoint.add(body)
TogglTracker(id=58687689, name="What a wonderful tracker description!", project=2123132, ...)
>>> await tracker_endpoint.delete(tracker)
None
PARAMETER DESCRIPTION
workspace_id

The workspace the Toggl trackers belong to.

TYPE: int | TogglWorkspace

auth

Authentication for the client.

TYPE: BasicAuth

cache

Where to cache trackers. Currently async only supports SQLite.

TYPE: AsyncSqliteCache[TogglTracker] | None DEFAULT: None

client

Optional async client to be passed to be used for requests.

TYPE: AsyncClient | None DEFAULT: None

timeout

How long it takes for the client to timeout. Keyword Only. Defaults to 10 seconds.

TYPE: int DEFAULT: 10

re_raise

Whether to raise all HTTPStatusError errors and not handle them internally. Keyword Only.

TYPE: bool DEFAULT: False

retries

Max retries to attempt if the server returns a 5xx status_code. Has no effect if re_raise is True. Keyword Only.

TYPE: int DEFAULT: 3

METHOD DESCRIPTION
current

Get current running tracker. Returns None if no tracker is running.

collect

Get a set of trackers depending on specified parameters.

get

Get a single tracker by ID.

edit

Edit an existing tracker based on the supplied parameters within the body.

bulk_edit

Bulk edit multiple trackers at the same time.

delete

Delete a tracker from Toggl.

stop

Stop the current running tracker.

add

Add a new tracker.

current async

current(*, refresh: bool = True) -> TogglTracker | None

Get current running tracker. Returns None if no tracker is running.

Official Documentation

Examples:

>>> await tracker_endpoint.current()
None
>>> await tracker_endpoint.current(refresh=True)
TogglTracker(...)
PARAMETER DESCRIPTION
refresh

Whether to check the remote API for running trackers. If 'refresh' is True it will check if there are any other running trackers and update if the 'stop' attribute is None.

TYPE: bool DEFAULT: True

RAISES DESCRIPTION
HTTPStatusError

If the request is not a success or any error that's not a '405' status code.

RETURNS DESCRIPTION
TogglTracker | None

A model from cache or the API. None if nothing is running.

collect async

collect(
    since: int | datetime | None = None,
    before: date | None = None,
    start_date: date | None = None,
    end_date: date | None = None,
    *,
    refresh: bool = False,
) -> list[TogglTracker]

Get a set of trackers depending on specified parameters.

Official Documentation

Missing meta and include_sharing query flags not supported by wrapper at the moment.

Examples:

>>> await tracker_ep.collect(since=17300032362, before=date(2024, 11, 27))
>>> await tracker_ep.collect(refresh=True)
>>> await tracker_ep.collect(start_date=date(2024, 11, 27), end_date=date(2024, 12, 27))
PARAMETER DESCRIPTION
since

Get entries modified since this date using UNIX timestamp. Includes deleted ones if refreshing.

TYPE: int | datetime | None DEFAULT: None

before

Get entries with start time, before given date (YYYY-MM-DD) or with time in RFC3339 format.

TYPE: date | None DEFAULT: None

start_date

Get entries with start time, from start_date YYYY-MM-DD or with time in RFC3339 format. To be used with end_date.

TYPE: date | None DEFAULT: None

end_date

Get entries with start time, until end_date YYYY-MM-DD or with time in RFC3339 format. To be used with start_date.

TYPE: date | None DEFAULT: None

refresh

Whether to refresh the cache or not.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
DateTimeError

If the dates are not in the correct ranges.

HTTPStatusError

If the request is not a successful status code.

RETURNS DESCRIPTION
list[TogglTracker]

List of TogglTracker objects that are within specified parameters. Empty if none is matched.

get async

get(
    tracker_id: int | TogglTracker, *, refresh: bool = False
) -> TogglTracker | None

Get a single tracker by ID.

Official Documentation

PARAMETER DESCRIPTION
tracker_id

ID of the tracker to get.

TYPE: int | TogglTracker

refresh

Whether to refresh the cache or not.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
HTTPStatusError

If anything thats not a ok or 404 status code is returned.

RETURNS DESCRIPTION
TogglTracker | None

TogglTracker object or None if not found.

edit async

edit(
    tracker: TogglTracker | int, body: TrackerBody, *, meta: bool = False
) -> TogglTracker

Edit an existing tracker based on the supplied parameters within the body.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> body = TrackerBody(description="What a wonderful tracker description!", project_id=2123132)
>>> await tracker_endpoint.edit(58687684, body)
TogglTracker(id=58687684, name="What a wonderful tracker description!", project=2123132, ...)
PARAMETER DESCRIPTION
tracker

Target tracker model or id to edit.

TYPE: TogglTracker | int

body

Updated content to add.

TYPE: TrackerBody

meta

Should the response contain data for meta entities.

TYPE: bool DEFAULT: False

RAISES DESCRIPTION
HTTPStatusError

For anything thats not a ok status code.

RETURNS DESCRIPTION
TogglTracker

A new model if successful else None.

bulk_edit async

bulk_edit(*trackers: int | TogglTracker, body: TrackerBody) -> Edits

Bulk edit multiple trackers at the same time.

Patch will be executed partially when there are errors with some records. No transaction, no rollback.

There is a limit of editing 100 trackers at the same time, so the method will make multiple calls if the count exceeds that limit.

Official Documentation

Examples:

>>> body = TrackerBody(description="All these trackers belong to me!")
>>> await tracker_endpoint.bulk_edit(1235151, 214124, body)
Edits(successes=[1235151, 214124], failures=[])
PARAMETER DESCRIPTION
trackers

All trackers that need to be edited.

TYPE: int | TogglTracker DEFAULT: ()

body

The parameters that need to be edited.

TYPE: TrackerBody

RAISES DESCRIPTION
HTTPStatusError

For anything thats not a ok status code.

RETURNS DESCRIPTION
Edits

Successeful or failed ids editing the trackers.

delete async

delete(tracker: TogglTracker | int) -> None

Delete a tracker from Toggl.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> await tracker_endpoint.delete(58687684)
None
PARAMETER DESCRIPTION
tracker

Tracker object with ID to delete.

TYPE: TogglTracker | int

RAISES DESCRIPTION
HTTPStatusError

If anything thats not a '404' or 'ok' code is returned.

stop async

stop(tracker: TogglTracker | int) -> TogglTracker | None

Stop the current running tracker.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> await tracker_endpoint.stop(58687684)
TogglTracker(id=58687684, name="What a wonderful tracker description!", ...)
PARAMETER DESCRIPTION
tracker

Tracker id to stop. An integer or model.

TYPE: TogglTracker | int

RAISES DESCRIPTION
HTTPStatusError

For anything thats not 'ok' or a '409' status code.

RETURNS DESCRIPTION
TogglTracker | None

If the tracker was stopped or if the tracker wasn't running it will return None.

add async

add(body: TrackerBody) -> TogglTracker

Add a new tracker.

This endpoint always hit the external API in order to keep trackers consistent.

Official Documentation

Examples:

>>> body = TrackerBody(description="Tracker description!", project_id=2123132)
>>> await tracker_endpoint.edit(body)
TogglTracker(id=78895400, name="Tracker description!", project=2123132, ...)
PARAMETER DESCRIPTION
body

Body of the request. Description must be set. If start date is not set it will be set to current time with duration set to -1 for a running tracker.

TYPE: TrackerBody

RAISES DESCRIPTION
HTTPStatusError

For anything that wasn't an ok status code.

NamingError

Description must be set in order to create a new tracker.

RETURNS DESCRIPTION
TogglTracker

The tracker that was created.

Types

toggl_api.BulkEditParameter

Bases: TypedDict

Source code in src/toggl_api/_tracker.py
35
36
37
38
class BulkEditParameter(TypedDict):
    op: Literal["add", "remove", "replace"]
    path: str
    value: Any

toggl_api.Edits

Bases: NamedTuple

Source code in src/toggl_api/_tracker.py
41
42
43
class Edits(NamedTuple):
    successes: list[int]
    failures: list[int]