Skip to content

Endpoint

toggl_api.meta.enums.RequestMethod

Bases: Enum

Self explanatory enumerations describing the different request types primarily for selecting request methods.

Source code in toggl_api/meta/enums.py
 4
 5
 6
 7
 8
 9
10
11
12
class RequestMethod(enum.Enum):
    """Self explanatory enumerations describing the different request types
    primarily for selecting request methods."""

    GET = enum.auto()
    POST = enum.auto()
    PUT = enum.auto()
    DELETE = enum.auto()
    PATCH = enum.auto()

toggl_api.meta.base_endpoint.TogglEndpoint

Bases: ABC, Generic[T]

Base class with basic functionality for all API requests.

Attributes:

  • BASE_ENDPOINT (str) –

    Base URL of the Toggl API.

  • HEADERS (Final[dict]) –

    Default headers that the API requires for most endpoints.

Parameters:

  • workspace_id (int | None) –

    DEPRECATED and moved to child classes.

  • auth (BasicAuth) –

    Authentication for the client.

  • timeout (int, default: 10 ) –

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

  • re_raise (bool, default: False ) –

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

  • retries (int, default: 3 ) –

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

Source code in toggl_api/meta/base_endpoint.py
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
class TogglEndpoint(ABC, Generic[T]):
    """Base class with basic functionality for all API requests.

    Attributes:
        BASE_ENDPOINT: Base URL of the Toggl API.
        HEADERS: Default headers that the API requires for most endpoints.

    Params:
        workspace_id: DEPRECATED and moved to child classes.
        auth: Authentication for the client.
        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.
    """

    BASE_ENDPOINT: ClassVar[str] = "https://api.track.toggl.com/api/v9/"
    HEADERS: Final[dict] = {"content-type": "application/json"}

    __slots__ = ("__client", "re_raise", "retries", "workspace_id")

    def __init__(
        self,
        workspace_id: int | None,
        auth: httpx.BasicAuth,
        *,
        timeout: int = 10,
        re_raise: bool = False,
        retries: int = 3,
    ) -> None:
        if workspace_id:
            warnings.warn(
                "DEPRECATED: 'workspace_id' is being removed from the base Toggl endpoint!",
                DeprecationWarning,
                stacklevel=3,
            )

        self.workspace_id = workspace_id
        self.re_raise = re_raise
        self.retries = max(0, retries)

        # NOTE: USES BASE_ENDPOINT instead of endpoint property for base_url
        # as current httpx concatenation is causing appended slashes.
        self.__client = httpx.Client(
            base_url=self.BASE_ENDPOINT,
            timeout=timeout,
            auth=auth,
        )
        atexit.register(self.__client.close)

    def method(self, method: RequestMethod) -> Callable:
        match_dict: dict[RequestMethod, Callable] = {
            RequestMethod.GET: self.__client.get,
            RequestMethod.POST: self.__client.post,
            RequestMethod.PUT: self.__client.put,
            RequestMethod.DELETE: self.__client.delete,
            RequestMethod.PATCH: self.__client.patch,
        }
        return match_dict.get(method, self.__client.get)

    def request(
        self,
        parameters: str,
        headers: Optional[dict] = None,
        body: Optional[dict | list] = None,
        method: RequestMethod = RequestMethod.GET,
        *,
        raw: bool = False,
        retries: int | None = None,
    ) -> Any:
        """Main request method which handles putting together the final API
        request.

        Args:
            parameters (str): Request parameters with the endpoint excluded.
                Will concate with the endpoint property.
            headers (dict, optional): Custom request headers. Defaults to
                class property if set to None.
            body (dict | list, optional): Request body JSON data for specifying info.
                Defaults to None. Only used with none-GET or DELETE requests.
            method (RequestMethod): Request method to select. Defaults to GET.
            raw (bool): Whether to use the raw data. Defaults to False.
            retries (int): For recursive calls if the server fails multiple times.

        Raises:
            HTTPStatusError: If the request is not a success.

        Returns:
            Response data or None if request does not return any data.
        """
        if retries is None:
            retries = self.retries

        url = self.endpoint + parameters
        headers = headers or self.HEADERS

        if body and method not in {RequestMethod.DELETE, RequestMethod.GET}:
            response = self.method(method)(url, headers=headers, json=body)
        else:
            response = self.method(method)(url, headers=headers)

        if codes.is_error(response.status_code):
            msg = "Request failed with status code %s: %s"
            log.error(msg, response.status_code, response.text)

            if not self.re_raise and codes.is_server_error(response.status_code) and retries:
                delay = random.randint(1, 5)
                retries -= 1
                log.error(
                    (
                        "Status code %s is a server error. "
                        "Retrying request in %s seconds. "
                        "There are %s retries left."
                    ),
                    response.status_code,
                    delay,
                    retries,
                )
                # NOTE: According to https://engineering.toggl.com/docs/#generic-responses
                time.sleep(delay)
                return TogglEndpoint.request(
                    self,
                    parameters,
                    headers,
                    body,
                    method,
                    raw=raw,
                    retries=retries,
                )

            response.raise_for_status()

        try:
            data = response if raw else response.json()
        except ValueError:
            return None

        if self.model is None:
            return data

        if isinstance(data, list):
            data = self.process_models(data)
        elif isinstance(data, dict):
            data = self.model.from_kwargs(**data)

        return data

    def process_models(self, data: list[dict[str, Any]]) -> list[T]:
        return [self.model.from_kwargs(**mdl) for mdl in data]  # type: ignore[misc]

    @property
    @abstractmethod
    def endpoint(self) -> str: ...

    @property
    @abstractmethod
    def model(self) -> type[T]: ...

    @staticmethod
    def api_status() -> bool:
        """Method for verifying that the Toggl API is up."""
        try:
            result = httpx.get("https://api.track.toggl.com/api/v9/status").json()
        except (HTTPStatusError, JSONDecodeError):
            log.critical("Failed to get a response from the Toggl API!")
            log.exception("%s")
            return False

        return bool(result) and result.get("status") == "OK"

endpoint: str abstractmethod property

model: type[T] abstractmethod property

request(parameters: str, headers: Optional[dict] = None, body: Optional[dict | list] = None, method: RequestMethod = RequestMethod.GET, *, raw: bool = False, retries: int | None = None) -> Any

Main request method which handles putting together the final API request.

Parameters:

  • parameters (str) –

    Request parameters with the endpoint excluded. Will concate with the endpoint property.

  • headers (dict, default: None ) –

    Custom request headers. Defaults to class property if set to None.

  • body (dict | list, default: None ) –

    Request body JSON data for specifying info. Defaults to None. Only used with none-GET or DELETE requests.

  • method (RequestMethod, default: GET ) –

    Request method to select. Defaults to GET.

  • raw (bool, default: False ) –

    Whether to use the raw data. Defaults to False.

  • retries (int, default: None ) –

    For recursive calls if the server fails multiple times.

Raises:

  • HTTPStatusError

    If the request is not a success.

Returns:

  • Any

    Response data or None if request does not return any data.

Source code in toggl_api/meta/base_endpoint.py
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
def request(
    self,
    parameters: str,
    headers: Optional[dict] = None,
    body: Optional[dict | list] = None,
    method: RequestMethod = RequestMethod.GET,
    *,
    raw: bool = False,
    retries: int | None = None,
) -> Any:
    """Main request method which handles putting together the final API
    request.

    Args:
        parameters (str): Request parameters with the endpoint excluded.
            Will concate with the endpoint property.
        headers (dict, optional): Custom request headers. Defaults to
            class property if set to None.
        body (dict | list, optional): Request body JSON data for specifying info.
            Defaults to None. Only used with none-GET or DELETE requests.
        method (RequestMethod): Request method to select. Defaults to GET.
        raw (bool): Whether to use the raw data. Defaults to False.
        retries (int): For recursive calls if the server fails multiple times.

    Raises:
        HTTPStatusError: If the request is not a success.

    Returns:
        Response data or None if request does not return any data.
    """
    if retries is None:
        retries = self.retries

    url = self.endpoint + parameters
    headers = headers or self.HEADERS

    if body and method not in {RequestMethod.DELETE, RequestMethod.GET}:
        response = self.method(method)(url, headers=headers, json=body)
    else:
        response = self.method(method)(url, headers=headers)

    if codes.is_error(response.status_code):
        msg = "Request failed with status code %s: %s"
        log.error(msg, response.status_code, response.text)

        if not self.re_raise and codes.is_server_error(response.status_code) and retries:
            delay = random.randint(1, 5)
            retries -= 1
            log.error(
                (
                    "Status code %s is a server error. "
                    "Retrying request in %s seconds. "
                    "There are %s retries left."
                ),
                response.status_code,
                delay,
                retries,
            )
            # NOTE: According to https://engineering.toggl.com/docs/#generic-responses
            time.sleep(delay)
            return TogglEndpoint.request(
                self,
                parameters,
                headers,
                body,
                method,
                raw=raw,
                retries=retries,
            )

        response.raise_for_status()

    try:
        data = response if raw else response.json()
    except ValueError:
        return None

    if self.model is None:
        return data

    if isinstance(data, list):
        data = self.process_models(data)
    elif isinstance(data, dict):
        data = self.model.from_kwargs(**data)

    return data

method(method: RequestMethod) -> Callable

Source code in toggl_api/meta/base_endpoint.py
86
87
88
89
90
91
92
93
94
def method(self, method: RequestMethod) -> Callable:
    match_dict: dict[RequestMethod, Callable] = {
        RequestMethod.GET: self.__client.get,
        RequestMethod.POST: self.__client.post,
        RequestMethod.PUT: self.__client.put,
        RequestMethod.DELETE: self.__client.delete,
        RequestMethod.PATCH: self.__client.patch,
    }
    return match_dict.get(method, self.__client.get)

process_models(data: list[dict[str, Any]]) -> list[T]

Source code in toggl_api/meta/base_endpoint.py
183
184
def process_models(self, data: list[dict[str, Any]]) -> list[T]:
    return [self.model.from_kwargs(**mdl) for mdl in data]  # type: ignore[misc]

api_status() -> bool staticmethod

Method for verifying that the Toggl API is up.

Source code in toggl_api/meta/base_endpoint.py
194
195
196
197
198
199
200
201
202
203
204
@staticmethod
def api_status() -> bool:
    """Method for verifying that the Toggl API is up."""
    try:
        result = httpx.get("https://api.track.toggl.com/api/v9/status").json()
    except (HTTPStatusError, JSONDecodeError):
        log.critical("Failed to get a response from the Toggl API!")
        log.exception("%s")
        return False

    return bool(result) and result.get("status") == "OK"

toggl_api.meta.cached_endpoint.TogglCachedEndpoint

Bases: TogglEndpoint[T]

Abstract cached endpoint for requesting toggl API data to disk.

See parent endpoint for more details.

Parameters:

  • auth (BasicAuth) –

    Authentication for the client.

  • cache (TogglCache[T]) –

    Cache object for caching toggl API data to disk. Builtin cache types are JSONCache and SqliteCache.

  • timeout (int, default: 10 ) –

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

  • re_raise (bool, default: False ) –

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

  • retries (int, default: 3 ) –

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

Attributes:

  • cache (TogglCache[T]) –

    Cache object the endpoint will use for storing models. Assigns itself as the parent automatically.

Methods:

  • request

    Overriden method that implements the cache into the request chain.

  • load_cache

    Method for loading cache into memory.

  • save_cache

    Method for saving cache to disk. Ignored if expiry is set to 0 seconds.

  • query

    Wrapper method for accessing querying capabilities within the assigned cache.

Source code in toggl_api/meta/cached_endpoint.py
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
class TogglCachedEndpoint(TogglEndpoint[T]):
    """Abstract cached endpoint for requesting toggl API data to disk.

    See parent endpoint for more details.

    Params:
        auth: Authentication for the client.
        cache: Cache object for caching toggl API data to disk. Builtin cache
            types are JSONCache and SqliteCache.
        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.

    Attributes:
        cache: Cache object the endpoint will use for storing models. Assigns
            itself as the parent automatically.

    Methods:
        request: Overriden method that implements the cache into the request chain.
        load_cache: Method for loading cache into memory.
        save_cache: Method for saving cache to disk. Ignored if expiry is set
            to 0 seconds.
        query: Wrapper method for accessing querying capabilities within the
            assigned cache.
    """

    __slots__ = ("_cache",)

    def __init__(
        self,
        workspace_id: int,
        auth: BasicAuth,
        cache: TogglCache[T],
        *,
        timeout: int = 10,
        re_raise: bool = False,
        retries: int = 3,
    ) -> None:
        super().__init__(
            workspace_id=workspace_id,
            auth=auth,
            timeout=timeout,
            re_raise=re_raise,
            retries=retries,
        )
        self.cache = cache

    def request(  # type: ignore[override]
        self,
        parameters: str,
        headers: Optional[dict] = None,
        body: Optional[dict | list] = None,
        method: RequestMethod = RequestMethod.GET,
        *,
        refresh: bool = False,
        raw: bool = False,
    ) -> Any:
        """Overridden request method with builtin cache.

        Args:
            parameters: Request parameters with the endpoint excluded.
            headers: Request headers. Custom headers can be added here.
            body: Request body for GET, POST, PUT, PATCH requests.
                Defaults to None.
            method: Request method. Defaults to GET.
            refresh: Whether to refresh the cache or not. Defaults to False.
            raw (bool): Whether to use the raw data. Defaults to False.

        Raises:
            HTTPStatusError: If the request is not a success.

        Returns:
            Toggl API response data processed into TogglClass objects or not
                depending on arguments.
        """

        data = self.load_cache() if self.model is not None else None
        if data and not refresh:
            log.info(
                "Loading request %s%s data from cache.",
                self.endpoint,
                parameters,
                extra={"body": body, "headers": headers, "method": method},
            )
            return data

        response = super().request(
            parameters,
            method=method,
            headers=headers,
            body=body,
            raw=raw,
        )
        if raw:
            return response

        if response is None or method == RequestMethod.DELETE:
            return None

        if self.model is not None:
            self.save_cache(response, method)  # type: ignore[arg-type]

        return response

    def load_cache(self) -> Iterable[T]:
        """Direct loading method for retrieving all models from cache."""
        return self.cache.load_cache()

    def save_cache(
        self,
        response: list[T] | T,
        method: RequestMethod,
    ) -> None:
        """Direct saving method for retrieving all models from cache."""
        if isinstance(self.cache.expire_after, timedelta) and not self.cache.expire_after.total_seconds():
            return None
        return self.cache.save_cache(response, method)

    def query(self, *query: TogglQuery, distinct: bool = False) -> list[T]:
        """Query wrapper for the cache method.

        If the original data structure is required use the query on the
        *.cache* attribute instead.

        Args:
            query: An arbitary amount of queries to match the models to.
            distinct: A boolean that remove duplicate values if present.

        Returns:
            A list objects depending on the endpoint.
        """
        return list(self.cache.query(*query, distinct=distinct))

    @property
    @abstractmethod
    def endpoint(self) -> str:
        pass

    @property
    def cache(self) -> TogglCache[T]:
        return self._cache

    @cache.setter
    def cache(self, value: TogglCache) -> None:
        self._cache = value
        if self.cache._parent is not self:  # noqa: SLF001
            self.cache.parent = self

cache: TogglCache[T] property writable

request(parameters: str, headers: Optional[dict] = None, body: Optional[dict | list] = None, method: RequestMethod = RequestMethod.GET, *, refresh: bool = False, raw: bool = False) -> Any

Overridden request method with builtin cache.

Parameters:

  • parameters (str) –

    Request parameters with the endpoint excluded.

  • headers (Optional[dict], default: None ) –

    Request headers. Custom headers can be added here.

  • body (Optional[dict | list], default: None ) –

    Request body for GET, POST, PUT, PATCH requests. Defaults to None.

  • method (RequestMethod, default: GET ) –

    Request method. Defaults to GET.

  • refresh (bool, default: False ) –

    Whether to refresh the cache or not. Defaults to False.

  • raw (bool, default: False ) –

    Whether to use the raw data. Defaults to False.

Raises:

  • HTTPStatusError

    If the request is not a success.

Returns:

  • Any

    Toggl API response data processed into TogglClass objects or not depending on arguments.

Source code in toggl_api/meta/cached_endpoint.py
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
def request(  # type: ignore[override]
    self,
    parameters: str,
    headers: Optional[dict] = None,
    body: Optional[dict | list] = None,
    method: RequestMethod = RequestMethod.GET,
    *,
    refresh: bool = False,
    raw: bool = False,
) -> Any:
    """Overridden request method with builtin cache.

    Args:
        parameters: Request parameters with the endpoint excluded.
        headers: Request headers. Custom headers can be added here.
        body: Request body for GET, POST, PUT, PATCH requests.
            Defaults to None.
        method: Request method. Defaults to GET.
        refresh: Whether to refresh the cache or not. Defaults to False.
        raw (bool): Whether to use the raw data. Defaults to False.

    Raises:
        HTTPStatusError: If the request is not a success.

    Returns:
        Toggl API response data processed into TogglClass objects or not
            depending on arguments.
    """

    data = self.load_cache() if self.model is not None else None
    if data and not refresh:
        log.info(
            "Loading request %s%s data from cache.",
            self.endpoint,
            parameters,
            extra={"body": body, "headers": headers, "method": method},
        )
        return data

    response = super().request(
        parameters,
        method=method,
        headers=headers,
        body=body,
        raw=raw,
    )
    if raw:
        return response

    if response is None or method == RequestMethod.DELETE:
        return None

    if self.model is not None:
        self.save_cache(response, method)  # type: ignore[arg-type]

    return response

load_cache() -> Iterable[T]

Direct loading method for retrieving all models from cache.

Source code in toggl_api/meta/cached_endpoint.py
147
148
149
def load_cache(self) -> Iterable[T]:
    """Direct loading method for retrieving all models from cache."""
    return self.cache.load_cache()

save_cache(response: list[T] | T, method: RequestMethod) -> None

Direct saving method for retrieving all models from cache.

Source code in toggl_api/meta/cached_endpoint.py
151
152
153
154
155
156
157
158
159
def save_cache(
    self,
    response: list[T] | T,
    method: RequestMethod,
) -> None:
    """Direct saving method for retrieving all models from cache."""
    if isinstance(self.cache.expire_after, timedelta) and not self.cache.expire_after.total_seconds():
        return None
    return self.cache.save_cache(response, method)

query(*query: TogglQuery, distinct: bool = False) -> list[T]

Query wrapper for the cache method.

If the original data structure is required use the query on the .cache attribute instead.

Parameters:

  • query (TogglQuery, default: () ) –

    An arbitary amount of queries to match the models to.

  • distinct (bool, default: False ) –

    A boolean that remove duplicate values if present.

Returns:

  • list[T]

    A list objects depending on the endpoint.

Source code in toggl_api/meta/cached_endpoint.py
161
162
163
164
165
166
167
168
169
170
171
172
173
174
def query(self, *query: TogglQuery, distinct: bool = False) -> list[T]:
    """Query wrapper for the cache method.

    If the original data structure is required use the query on the
    *.cache* attribute instead.

    Args:
        query: An arbitary amount of queries to match the models to.
        distinct: A boolean that remove duplicate values if present.

    Returns:
        A list objects depending on the endpoint.
    """
    return list(self.cache.query(*query, distinct=distinct))