Coverage for compiler_admin / api / toggl.py: 100%
88 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 05:48 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-28 05:48 +0000
1from base64 import b64encode
2from datetime import datetime
4import requests
6from compiler_admin import __version__
9class TogglBase:
10 """Base class for Toggl API clients.
12 See https://engineering.toggl.com/docs/.
14 Sub-classes should implement `api_url_resource(self)`.
15 """
17 API_BASE_URL = "https://api.track.toggl.com"
18 API_VERSION = "api/v9"
19 API_HEADERS = {"Content-Type": "application/json", "User-Agent": "compilerla/compiler-admin:{}".format(__version__)}
21 def __init__(self, api_token: str, workspace_id: int, **kwargs):
22 self._token = api_token
23 self.workspace_id = workspace_id
25 self.headers = dict(TogglBase.API_HEADERS)
26 self.headers.update(self._authorization_header())
28 self.session = requests.Session()
29 self.session.headers.update(self.headers)
30 self.timeout = int(kwargs.get("timeout", 5))
32 def _authorization_header(self):
33 """Gets an `Authorization: Basic xyz` header using the Toggl API token.
35 See https://engineering.toggl.com/docs/authentication.
36 """
37 creds = f"{self._token}:api_token"
38 creds64 = b64encode(bytes(creds, "utf-8")).decode("utf-8")
39 return {"Authorization": "Basic {}".format(creds64)}
41 @property
42 def api_url_resource(self):
43 """Sub-classes should implement this prop to use make_api_url() for the given API resource."""
44 raise NotImplementedError("Implement this property to use make_api_url().")
46 @property
47 def api_url_version(self):
48 """The version information for an API request URL."""
49 return self.API_VERSION
51 def make_api_url(self, endpoint: str):
52 """Get a fully formed and versioned URL for an endpoint within the Toggl API."""
53 return "/".join((TogglBase.API_BASE_URL, self.api_url_version, self.api_url_resource, endpoint))
55 def _get(self, url: str, **kwargs) -> requests.Response:
56 response = self.session.get(url, params=kwargs, timeout=self.timeout)
57 response.raise_for_status()
58 return response
60 def _post(self, url: str, **kwargs) -> requests.Response:
61 response = self.session.post(url, json=kwargs, timeout=self.timeout)
62 response.raise_for_status()
63 return response
66class TogglOrganization(TogglBase):
67 ORGANIZATIONS_ID = "organizations/{}"
69 def __init__(self, api_token, workspace_id, organization_id, **kwargs):
70 super().__init__(api_token, workspace_id, **kwargs)
71 self.organization_id = organization_id
73 @property
74 def api_url_resource(self):
75 """The organizations portion of an API URL."""
76 return self.ORGANIZATIONS_ID.format(self.organization_id)
78 def get_groups(self, name: str = None) -> requests.Response:
79 """Request a list of groups from the Toggl organization.
81 See
82 https://engineering.toggl.com/docs/track/api/groups/#get-list-of-groups-in-organization-with-user-and-workspace-assignments.
84 Args:
85 name (str): Return groups with a name containing the provided value.
87 Returns:
88 response (requests.Response): The HTTP response.
89 """
90 kwargs = {"workspace": str(self.workspace_id)}
91 if name:
92 kwargs["name"] = name
93 url = self.make_api_url("groups")
95 return self._get(url, **kwargs)
97 def get_users(self, **kwargs) -> requests.Response:
98 """Request a list of users from the Toggl organization.
100 See https://engineering.toggl.com/docs/track/api/organizations/#get-list-of-users-in-organization.
102 Returns:
103 response (requests.Response): The HTTP response.
104 """
105 kwargs["workspaces"] = str(self.workspace_id)
106 url = self.make_api_url("users")
108 return self._get(url, **kwargs)
111class TogglReports(TogglBase):
112 REPORTS_API_VERSION = "reports/api/v3"
113 WORKSPACE_ID = "workspace/{}"
115 @property
116 def api_url_resource(self):
117 return self.WORKSPACE_ID.format(self.workspace_id)
119 @property
120 def api_url_version(self):
121 return self.REPORTS_API_VERSION
123 def detailed_time_entries(self, start_date: datetime, end_date: datetime, **kwargs) -> requests.Response:
124 """Request a CSV report from Toggl of detailed time entries for the given date range.
126 Args:
127 start_date (datetime): The beginning of the reporting period.
129 end_date (datetime): The end of the reporting period.
131 Extra `kwargs` are passed through as a POST json body.
133 By default, requests a report with the following configuration:
134 * `rounding=1` (True, but this is an int param)
135 * `rounding_minutes=15`
137 See https://engineering.toggl.com/docs/reports/detailed_reports#post-export-detailed-report.
139 Returns:
140 response (requests.Response): The HTTP response.
141 """
142 # ensure start_date precedes end_date
143 start_date, end_date = min(start_date, end_date), max(start_date, end_date)
144 start = start_date.strftime("%Y-%m-%d")
145 end = end_date.strftime("%Y-%m-%d")
147 # calculate a timeout based on the size of the reporting period in days
148 # approximately 5 seconds per month of query size, with a minimum of 5 seconds
149 range_days = (end_date - start_date).days
150 current_timeout = self.timeout
151 dynamic_timeout = int((max(30, range_days) / 30.0) * 5)
152 self.timeout = max(current_timeout, dynamic_timeout)
154 params = dict(
155 start_date=start,
156 end_date=end,
157 rounding=1,
158 rounding_minutes=15,
159 )
160 params.update(kwargs)
162 url = self.make_api_url("search/time_entries.csv")
163 response = self._post(url, **params)
164 self.timeout = current_timeout
166 return response
169class TogglWorkspace(TogglBase):
170 WORKSPACES_ID = "workspaces/{}"
172 @property
173 def api_url_resource(self):
174 """The workspaces portion of an API URL."""
175 return self.WORKSPACES_ID.format(self.workspace_id)
177 def get_users(self, **kwargs) -> requests.Response:
178 """Request a list of users from the Toggl workspace.
180 See https://engineering.toggl.com/docs/track/api/workspaces/#get-get-workspace-users.
182 Returns:
183 response (requests.Response): The HTTP response.
184 """
185 url = self.make_api_url("users")
187 return self._get(url, **kwargs)
189 def update_preferences(self, **kwargs) -> requests.Response:
190 """Update workspace preferences.
192 See https://engineering.toggl.com/docs/api/preferences/#post-update-workspace-preferences.
193 """
194 url = self.make_api_url("preferences")
196 return self._post(url, **kwargs)