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

1from base64 import b64encode 

2from datetime import datetime 

3 

4import requests 

5 

6from compiler_admin import __version__ 

7 

8 

9class TogglBase: 

10 """Base class for Toggl API clients. 

11 

12 See https://engineering.toggl.com/docs/. 

13 

14 Sub-classes should implement `api_url_resource(self)`. 

15 """ 

16 

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__)} 

20 

21 def __init__(self, api_token: str, workspace_id: int, **kwargs): 

22 self._token = api_token 

23 self.workspace_id = workspace_id 

24 

25 self.headers = dict(TogglBase.API_HEADERS) 

26 self.headers.update(self._authorization_header()) 

27 

28 self.session = requests.Session() 

29 self.session.headers.update(self.headers) 

30 self.timeout = int(kwargs.get("timeout", 5)) 

31 

32 def _authorization_header(self): 

33 """Gets an `Authorization: Basic xyz` header using the Toggl API token. 

34 

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)} 

40 

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().") 

45 

46 @property 

47 def api_url_version(self): 

48 """The version information for an API request URL.""" 

49 return self.API_VERSION 

50 

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)) 

54 

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 

59 

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 

64 

65 

66class TogglOrganization(TogglBase): 

67 ORGANIZATIONS_ID = "organizations/{}" 

68 

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 

72 

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) 

77 

78 def get_groups(self, name: str = None) -> requests.Response: 

79 """Request a list of groups from the Toggl organization. 

80 

81 See 

82 https://engineering.toggl.com/docs/track/api/groups/#get-list-of-groups-in-organization-with-user-and-workspace-assignments. 

83 

84 Args: 

85 name (str): Return groups with a name containing the provided value. 

86 

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") 

94 

95 return self._get(url, **kwargs) 

96 

97 def get_users(self, **kwargs) -> requests.Response: 

98 """Request a list of users from the Toggl organization. 

99 

100 See https://engineering.toggl.com/docs/track/api/organizations/#get-list-of-users-in-organization. 

101 

102 Returns: 

103 response (requests.Response): The HTTP response. 

104 """ 

105 kwargs["workspaces"] = str(self.workspace_id) 

106 url = self.make_api_url("users") 

107 

108 return self._get(url, **kwargs) 

109 

110 

111class TogglReports(TogglBase): 

112 REPORTS_API_VERSION = "reports/api/v3" 

113 WORKSPACE_ID = "workspace/{}" 

114 

115 @property 

116 def api_url_resource(self): 

117 return self.WORKSPACE_ID.format(self.workspace_id) 

118 

119 @property 

120 def api_url_version(self): 

121 return self.REPORTS_API_VERSION 

122 

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. 

125 

126 Args: 

127 start_date (datetime): The beginning of the reporting period. 

128 

129 end_date (datetime): The end of the reporting period. 

130 

131 Extra `kwargs` are passed through as a POST json body. 

132 

133 By default, requests a report with the following configuration: 

134 * `rounding=1` (True, but this is an int param) 

135 * `rounding_minutes=15` 

136 

137 See https://engineering.toggl.com/docs/reports/detailed_reports#post-export-detailed-report. 

138 

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") 

146 

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) 

153 

154 params = dict( 

155 start_date=start, 

156 end_date=end, 

157 rounding=1, 

158 rounding_minutes=15, 

159 ) 

160 params.update(kwargs) 

161 

162 url = self.make_api_url("search/time_entries.csv") 

163 response = self._post(url, **params) 

164 self.timeout = current_timeout 

165 

166 return response 

167 

168 

169class TogglWorkspace(TogglBase): 

170 WORKSPACES_ID = "workspaces/{}" 

171 

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) 

176 

177 def get_users(self, **kwargs) -> requests.Response: 

178 """Request a list of users from the Toggl workspace. 

179 

180 See https://engineering.toggl.com/docs/track/api/workspaces/#get-get-workspace-users. 

181 

182 Returns: 

183 response (requests.Response): The HTTP response. 

184 """ 

185 url = self.make_api_url("users") 

186 

187 return self._get(url, **kwargs) 

188 

189 def update_preferences(self, **kwargs) -> requests.Response: 

190 """Update workspace preferences. 

191 

192 See https://engineering.toggl.com/docs/api/preferences/#post-update-workspace-preferences. 

193 """ 

194 url = self.make_api_url("preferences") 

195 

196 return self._post(url, **kwargs)