Skip to content

Client

toggl_api.ClientBody dataclass

Bases: BaseBody

JSON body dataclass for PUT, POST & PATCH requests.

name: Optional[str] = field(default=None) class-attribute instance-attribute

Name of the client. Defaults to None. Will be required if its a POST request.

status: Optional[CLIENT_STATUS] = field(default=None) class-attribute instance-attribute

Status of the client. API defaults to active. Premium Feature.

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

Formats the body for JSON requests.

Gets called by the endpoint methods before requesting.

Parameters:

  • endpoint (str) –

    API endpoint for filtering purposes.

  • body (Any, default: {} ) –

    Any additonal body content that the endpoint request requires. If passing workspace id to client endpoints use 'wid' instead.

Returns:

  • dict ( dict[str, Any] ) –

    JSON compatible formatted body.

toggl_api.ClientEndpoint

Bases: TogglCachedEndpoint[TogglClient]

Specific endpoints for retrieving and modifying clients.

Official Documentation

Examples:

>>> wid = 123213324
>>> client_endpoint = ClientEndpoint(wid, BasicAuth(...), SqliteCache(...))
>>> client_endpoint.get(214125562)
TogglClient(214125562, "Simplicidentata", workspace=123213324)

Parameters:

  • workspace_id (int | TogglWorkspace) –

    The workspace the clients belong to.

  • auth (BasicAuth) –

    Authentication for the client.

  • cache (TogglCache[TogglClient]) –

    Cache object where the clients will stored and handled.

  • 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/client.py
 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
258
259
260
261
262
263
264
265
266
267
268
269
class ClientEndpoint(TogglCachedEndpoint[TogglClient]):
    """Specific endpoints for retrieving and modifying clients.

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

    Examples:
        >>> wid = 123213324
        >>> client_endpoint = ClientEndpoint(wid, BasicAuth(...), SqliteCache(...))
        >>> client_endpoint.get(214125562)
        TogglClient(214125562, "Simplicidentata", workspace=123213324)

    Params:
        workspace_id: The workspace the clients belong to.
        auth: Authentication for the client.
        cache: Cache object where the clients will stored and handled.
        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.
    """

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

    def add(self, body: ClientBody) -> TogglClient | None:
        """Create a Client based on parameters set in the provided body.

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

        [Official Documentation](https://engineering.toggl.com/docs/api/clients#post-create-client)

        Args:
            body: New parameters for the client to be created.

        Raises:
            NamingError: If no name was set as its required.

        Returns:
            Newly created client with specified parameters.
        """

        if body.name is None:
            msg = "Name must be set in order to create a client!"
            raise NamingError(msg)

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

    def get(self, client_id: int | TogglClient, *, refresh: bool = False) -> TogglClient | None:
        """Request a client based on its id.

        [Official Documentation](https://engineering.toggl.com/docs/api/clients#get-load-client-from-id)

        Args:
            client_id: Which client to look for.
            refresh: Whether to only check cache. It will default to True if id
                is not found in cache. Defaults to False.

        Returns:
            A TogglClient if the client was found else None.
        """
        if isinstance(client_id, TogglClient):
            client_id = client_id.id

        if not refresh:
            return self.cache.find_entry({"id": client_id})  # type: ignore[return-value]

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

        return response or None

    def edit(self, client: TogglClient | int, body: ClientBody) -> TogglClient | None:
        """Edit a client with the supplied parameters from the body.

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

        [Official Documentation](https://engineering.toggl.com/docs/api/clients#put-change-client)

        Args:
            client: Target client to edit.
            body: New parameters to use. Ignore client status.

        Returns:
            Newly edited client or None if not found.
        """
        if body.status:
            log.warning("Client status not supported by edit endpoint")
            body.status = None

        if isinstance(client, TogglClient):
            client = client.id
        return self.request(
            f"/{client}",
            body=body.format("edit", wid=self.workspace_id),
            method=RequestMethod.PUT,
            refresh=True,
        )

    def delete(self, client: TogglClient | int) -> None:
        """Delete a client based on its ID.

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

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

        Raises:
            HTTPStatusError: If anything thats not an *ok* or *404* status code
                is returned.
        """
        client_id = client if isinstance(client, int) else client.id
        try:
            self.request(f"/{client_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(
                "Client with id %s was either already deleted or did not exist in the first place!",
                client_id,
            )
        if isinstance(client, int):
            clt = self.cache.find_entry({"id": client})
            if not isinstance(clt, TogglClient):
                return
            client = clt

        self.cache.delete_entries(client)
        self.cache.commit()

    def _collect_cache(self, body: ClientBody | None) -> list[TogglClient]:
        if body and body.status is not None:
            log.warning("Client body 'status' parameter is not implemented with cache!")

        if body and body.name:
            return list(self.query(TogglQuery("name", body.name)))

        return list(self.load_cache())

    def collect(
        self,
        body: Optional[ClientBody] = None,
        *,
        refresh: bool = False,
    ) -> list[TogglClient]:
        """Request all Clients based on status and name if specified in the body.

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

        Args:
            body: Status and name to target. Ignores notes. Ignores status if using cache.
            refresh: Whether to refresh cache.

        Returns:
            A list of clients. Empty if not found.
        """
        if not refresh:
            return self._collect_cache(body)

        url = ""
        if body and body.status:
            url += f"?{body.status}"
        if body and body.name:
            if body.status:
                url += "&"
            else:
                url += "?"
            url += f"{body.name}"

        response = self.request(url, method=RequestMethod.GET, refresh=refresh)
        return response if isinstance(response, list) else []

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

    @property
    def model(self) -> type[TogglClient]:
        return TogglClient

add(body: ClientBody) -> TogglClient | None

Create a Client based on parameters set in the provided body.

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

Official Documentation

Parameters:

  • body (ClientBody) –

    New parameters for the client to be created.

Raises:

Returns:

  • TogglClient | None

    Newly created client with specified parameters.

Source code in toggl_api/client.py
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
def add(self, body: ClientBody) -> TogglClient | None:
    """Create a Client based on parameters set in the provided body.

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

    [Official Documentation](https://engineering.toggl.com/docs/api/clients#post-create-client)

    Args:
        body: New parameters for the client to be created.

    Raises:
        NamingError: If no name was set as its required.

    Returns:
        Newly created client with specified parameters.
    """

    if body.name is None:
        msg = "Name must be set in order to create a client!"
        raise NamingError(msg)

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

get(client_id: int | TogglClient, *, refresh: bool = False) -> TogglClient | None

Request a client based on its id.

Official Documentation

Parameters:

  • client_id (int | TogglClient) –

    Which client to look for.

  • refresh (bool, default: False ) –

    Whether to only check cache. It will default to True if id is not found in cache. Defaults to False.

Returns:

  • TogglClient | None

    A TogglClient if the client was found else None.

Source code in toggl_api/client.py
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
def get(self, client_id: int | TogglClient, *, refresh: bool = False) -> TogglClient | None:
    """Request a client based on its id.

    [Official Documentation](https://engineering.toggl.com/docs/api/clients#get-load-client-from-id)

    Args:
        client_id: Which client to look for.
        refresh: Whether to only check cache. It will default to True if id
            is not found in cache. Defaults to False.

    Returns:
        A TogglClient if the client was found else None.
    """
    if isinstance(client_id, TogglClient):
        client_id = client_id.id

    if not refresh:
        return self.cache.find_entry({"id": client_id})  # type: ignore[return-value]

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

    return response or None

edit(client: TogglClient | int, body: ClientBody) -> TogglClient | None

Edit a client with the supplied parameters from the body.

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

Official Documentation

Parameters:

  • client (TogglClient | int) –

    Target client to edit.

  • body (ClientBody) –

    New parameters to use. Ignore client status.

Returns:

  • TogglClient | None

    Newly edited client or None if not found.

Source code in toggl_api/client.py
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
def edit(self, client: TogglClient | int, body: ClientBody) -> TogglClient | None:
    """Edit a client with the supplied parameters from the body.

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

    [Official Documentation](https://engineering.toggl.com/docs/api/clients#put-change-client)

    Args:
        client: Target client to edit.
        body: New parameters to use. Ignore client status.

    Returns:
        Newly edited client or None if not found.
    """
    if body.status:
        log.warning("Client status not supported by edit endpoint")
        body.status = None

    if isinstance(client, TogglClient):
        client = client.id
    return self.request(
        f"/{client}",
        body=body.format("edit", wid=self.workspace_id),
        method=RequestMethod.PUT,
        refresh=True,
    )

delete(client: TogglClient | int) -> None

Delete a client based on its ID.

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

Official Documentation

Raises:

  • HTTPStatusError

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

Source code in toggl_api/client.py
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
def delete(self, client: TogglClient | int) -> None:
    """Delete a client based on its ID.

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

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

    Raises:
        HTTPStatusError: If anything thats not an *ok* or *404* status code
            is returned.
    """
    client_id = client if isinstance(client, int) else client.id
    try:
        self.request(f"/{client_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(
            "Client with id %s was either already deleted or did not exist in the first place!",
            client_id,
        )
    if isinstance(client, int):
        clt = self.cache.find_entry({"id": client})
        if not isinstance(clt, TogglClient):
            return
        client = clt

    self.cache.delete_entries(client)
    self.cache.commit()

collect(body: Optional[ClientBody] = None, *, refresh: bool = False) -> list[TogglClient]

Request all Clients based on status and name if specified in the body.

Official Documentation

Parameters:

  • body (Optional[ClientBody], default: None ) –

    Status and name to target. Ignores notes. Ignores status if using cache.

  • refresh (bool, default: False ) –

    Whether to refresh cache.

Returns:

  • list[TogglClient]

    A list of clients. Empty if not found.

Source code in toggl_api/client.py
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
def collect(
    self,
    body: Optional[ClientBody] = None,
    *,
    refresh: bool = False,
) -> list[TogglClient]:
    """Request all Clients based on status and name if specified in the body.

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

    Args:
        body: Status and name to target. Ignores notes. Ignores status if using cache.
        refresh: Whether to refresh cache.

    Returns:
        A list of clients. Empty if not found.
    """
    if not refresh:
        return self._collect_cache(body)

    url = ""
    if body and body.status:
        url += f"?{body.status}"
    if body and body.name:
        if body.status:
            url += "&"
        else:
            url += "?"
        url += f"{body.name}"

    response = self.request(url, method=RequestMethod.GET, refresh=refresh)
    return response if isinstance(response, list) else []