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.

  • client

    Httpx client that is used for making requests to the API.

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
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
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.
        client: Httpx client that is used for making requests to the API.

    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/"
    ENDPOINT: ClassVar[str]
    HEADERS: Final[dict] = {"content-type": "application/json"}
    MODEL: type[T] | None = None

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

    def __init__(
        self,
        workspace_id: int | None,
        auth: 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 = Client(
            base_url=self.BASE_ENDPOINT,
            timeout=timeout,
            auth=auth,
        )
        atexit.register(self.client.close)

    def method(self, method: RequestMethod) -> Callable:
        warnings.warn(
            "DEPRECATED: Use `httpx.Client.build_request` instead!",
            DeprecationWarning,
            stacklevel=2,
        )
        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_handle_error(
        self,
        response: Response,
        body: dict | list | None,
        headers: dict | None,
        method: RequestMethod,
        parameters: str,
        *,
        raw: bool,
        retries: int,
    ) -> T | list[T] | Response | None:
        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,
            )

        return response.raise_for_status()

    def _process_response(self, response: Response, *, raw: bool) -> T | list[T] | Response | None:
        try:
            data = response if raw else response.json()
        except ValueError:
            return None

        if self.MODEL is None or raw:
            return data

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

        return data  # type: ignore[return-value]

    def _build_request(
        self,
        parameters: str,
        headers: dict | None,
        body: dict | list | None,
        method: RequestMethod,
    ) -> Request:
        url = self.BASE_ENDPOINT + self.endpoint + parameters
        headers = headers or self.HEADERS

        requires_body = method not in {RequestMethod.DELETE, RequestMethod.GET}
        return self.client.build_request(
            method.name.lower(),
            url,
            headers=headers,
            json=body if requires_body else None,
        )

    def request(
        self,
        parameters: str,
        headers: dict | None = None,
        body: dict | list | None = None,
        method: RequestMethod = RequestMethod.GET,
        *,
        raw: bool = False,
        retries: int | None = None,
    ) -> T | list[T] | Response | None:
        """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

        request = self._build_request(parameters, headers, body, method)
        response = self.client.send(request)

        if codes.is_error(response.status_code):
            return self._request_handle_error(
                response,
                body,
                headers,
                method,
                parameters,
                raw=raw,
                retries=retries,
            )

        return self._process_response(response, raw=raw)

    @classmethod
    def process_models(cls, data: list[dict[str, Any]]) -> list[T]:
        assert cls.MODEL is not None
        return [cls.MODEL.from_kwargs(**mdl) for mdl in data]

    @property
    def model(self) -> type[T] | None:
        warnings.warn(
            "DEPRECATED: Use 'Endpoint.MODEL' ClassVar instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.MODEL

    @property
    def endpoint(self) -> str:
        warnings.warn(
            "DEPRECATED: Use 'Endpoint.BASE_ENDPOINT' ClassVar instead.",
            DeprecationWarning,
            stacklevel=2,
        )
        return self.BASE_ENDPOINT

    @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 property

model: type[T] | None property

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

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:

  • T | list[T] | Response | None

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

Source code in toggl_api/meta/base_endpoint.py
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
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
def request(
    self,
    parameters: str,
    headers: dict | None = None,
    body: dict | list | None = None,
    method: RequestMethod = RequestMethod.GET,
    *,
    raw: bool = False,
    retries: int | None = None,
) -> T | list[T] | Response | None:
    """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

    request = self._build_request(parameters, headers, body, method)
    response = self.client.send(request)

    if codes.is_error(response.status_code):
        return self._request_handle_error(
            response,
            body,
            headers,
            method,
            parameters,
            raw=raw,
            retries=retries,
        )

    return self._process_response(response, raw=raw)

method(method: RequestMethod) -> Callable

Source code in toggl_api/meta/base_endpoint.py
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
def method(self, method: RequestMethod) -> Callable:
    warnings.warn(
        "DEPRECATED: Use `httpx.Client.build_request` instead!",
        DeprecationWarning,
        stacklevel=2,
    )
    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] classmethod

Source code in toggl_api/meta/base_endpoint.py
224
225
226
227
@classmethod
def process_models(cls, data: list[dict[str, Any]]) -> list[T]:
    assert cls.MODEL is not None
    return [cls.MODEL.from_kwargs(**mdl) for mdl in data]

api_status() -> bool staticmethod

Method for verifying that the Toggl API is up.

Source code in toggl_api/meta/base_endpoint.py
247
248
249
250
251
252
253
254
255
256
257
@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
190
191
192
193
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: dict[str, Any] | None = None,
        body: dict | list | None = None,
        method: RequestMethod = RequestMethod.GET,
        *,
        refresh: bool = False,
        raw: bool = False,
    ) -> T | list[T] | Response | None:
        """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 cast(list[T], 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():
            log.debug(
                "Cache is set to immediately expire!",
                extra={"expiry": self.cache.expire_after},
            )
            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: dict[str, Any] | None = None, body: dict | list | None = None, method: RequestMethod = RequestMethod.GET, *, refresh: bool = False, raw: bool = False) -> T | list[T] | Response | None

Overridden request method with builtin cache.

Parameters:

  • parameters (str) –

    Request parameters with the endpoint excluded.

  • headers (dict[str, Any] | None, default: None ) –

    Request headers. Custom headers can be added here.

  • body (dict | list | None, 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:

  • T | list[T] | Response | None

    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: dict[str, Any] | None = None,
    body: dict | list | None = None,
    method: RequestMethod = RequestMethod.GET,
    *,
    refresh: bool = False,
    raw: bool = False,
) -> T | list[T] | Response | None:
    """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 cast(list[T], 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
160
161
162
163
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():
        log.debug(
            "Cache is set to immediately expire!",
            extra={"expiry": self.cache.expire_after},
        )
        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
165
166
167
168
169
170
171
172
173
174
175
176
177
178
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))