|
1 | | -from base64 import b64encode |
2 | 1 | from datetime import datetime |
3 | 2 | import io |
4 | 3 | import os |
5 | 4 | import sys |
6 | 5 | from typing import TextIO |
7 | 6 |
|
8 | 7 | import pandas as pd |
9 | | -import requests |
10 | 8 |
|
11 | | -from compiler_admin import __version__ |
| 9 | +from compiler_admin.api.toggl import Toggl |
12 | 10 | from compiler_admin.services.google import user_info as google_user_info |
13 | 11 | import compiler_admin.services.files as files |
14 | 12 |
|
|
23 | 21 | OUTPUT_COLUMNS = ["Date", "Client", "Project", "Task", "Notes", "Hours", "First name", "Last name"] |
24 | 22 |
|
25 | 23 |
|
26 | | -class Toggl: |
27 | | - """Toggl API Client. |
28 | | -
|
29 | | - See https://engineering.toggl.com/docs/. |
30 | | - """ |
31 | | - |
32 | | - API_BASE_URL = "https://api.track.toggl.com" |
33 | | - API_REPORTS_BASE_URL = "reports/api/v3" |
34 | | - API_WORKSPACE = "workspace/{}" |
35 | | - API_HEADERS = {"Content-Type": "application/json", "User-Agent": "compilerla/compiler-admin:{}".format(__version__)} |
36 | | - |
37 | | - def __init__(self, api_token: str, workspace_id: int, **kwargs): |
38 | | - self._token = api_token |
39 | | - self.workspace_id = workspace_id |
40 | | - |
41 | | - self.headers = dict(Toggl.API_HEADERS) |
42 | | - self.headers.update(self._authorization_header()) |
43 | | - |
44 | | - self.timeout = int(kwargs.get("timeout", 5)) |
45 | | - |
46 | | - @property |
47 | | - def workspace_url_fragment(self): |
48 | | - """The workspace portion of an API URL.""" |
49 | | - return Toggl.API_WORKSPACE.format(self.workspace_id) |
50 | | - |
51 | | - def _authorization_header(self): |
52 | | - """Gets an `Authorization: Basic xyz` header using the Toggl API token. |
53 | | -
|
54 | | - See https://engineering.toggl.com/docs/authentication. |
55 | | - """ |
56 | | - creds = f"{self._token}:api_token" |
57 | | - creds64 = b64encode(bytes(creds, "utf-8")).decode("utf-8") |
58 | | - return {"Authorization": "Basic {}".format(creds64)} |
59 | | - |
60 | | - def _make_report_url(self, endpoint: str): |
61 | | - """Get a fully formed URL for the Toggl Reports API v3 endpoint. |
62 | | -
|
63 | | - See https://engineering.toggl.com/docs/reports_start. |
64 | | - """ |
65 | | - return "/".join((Toggl.API_BASE_URL, Toggl.API_REPORTS_BASE_URL, self.workspace_url_fragment, endpoint)) |
66 | | - |
67 | | - def post_reports(self, endpoint: str, **kwargs) -> requests.Response: |
68 | | - """Send a POST request to the Reports v3 `endpoint`. |
69 | | -
|
70 | | - Extra `kwargs` are passed through as a POST json body. |
71 | | -
|
72 | | - Will raise for non-200 status codes. |
73 | | -
|
74 | | - See https://engineering.toggl.com/docs/reports_start. |
75 | | - """ |
76 | | - url = self._make_report_url(endpoint) |
77 | | - |
78 | | - response = requests.post(url, json=kwargs, headers=self.headers, timeout=self.timeout) |
79 | | - response.raise_for_status() |
80 | | - |
81 | | - return response |
82 | | - |
83 | | - def detailed_time_entries(self, start_date: datetime, end_date: datetime, **kwargs): |
84 | | - """Request a CSV report from Toggl of detailed time entries for the given date range. |
85 | | -
|
86 | | - Args: |
87 | | - start_date (datetime): The beginning of the reporting period. |
88 | | -
|
89 | | - end_date (str): The end of the reporting period. |
90 | | -
|
91 | | - Extra `kwargs` are passed through as a POST json body. |
92 | | -
|
93 | | - By default, requests a report with the following configuration: |
94 | | - * `billable=True` |
95 | | - * `rounding=1` (True, but this is an int param) |
96 | | - * `rounding_minutes=15` |
97 | | -
|
98 | | - See https://engineering.toggl.com/docs/reports/detailed_reports#post-export-detailed-report. |
99 | | -
|
100 | | - Returns: |
101 | | - response (requests.Response): The HTTP response. |
102 | | - """ |
103 | | - # ensure start_date precedes end_date |
104 | | - start_date, end_date = min(start_date, end_date), max(start_date, end_date) |
105 | | - start = start_date.strftime("%Y-%m-%d") |
106 | | - end = end_date.strftime("%Y-%m-%d") |
107 | | - |
108 | | - # calculate a timeout based on the size of the reporting period in days |
109 | | - # approximately 5 seconds per month of query size, with a minimum of 5 seconds |
110 | | - range_days = (end_date - start_date).days |
111 | | - current_timeout = self.timeout |
112 | | - dynamic_timeout = int((max(30, range_days) / 30.0) * 5) |
113 | | - self.timeout = max(current_timeout, dynamic_timeout) |
114 | | - |
115 | | - params = dict( |
116 | | - billable=True, |
117 | | - start_date=start, |
118 | | - end_date=end, |
119 | | - rounding=1, |
120 | | - rounding_minutes=15, |
121 | | - ) |
122 | | - params.update(kwargs) |
123 | | - |
124 | | - response = self.post_reports("search/time_entries.csv", **params) |
125 | | - self.timeout = current_timeout |
126 | | - |
127 | | - return response |
128 | | - |
129 | | - |
130 | 24 | def _get_first_name(email: str) -> str: |
131 | 25 | """Get cached first name or derive from email.""" |
132 | 26 | user = USER_INFO.get(email) |
|
0 commit comments