2020-08-12 03:24:23 +02:00
|
|
|
"""
|
|
|
|
TogglPy is a non-cluttered, easily understood and implemented
|
|
|
|
library for interacting with the Toggl API.
|
|
|
|
"""
|
|
|
|
import json # parsing json data
|
|
|
|
import math
|
|
|
|
import sys
|
|
|
|
import time
|
|
|
|
from base64 import b64encode
|
|
|
|
from datetime import datetime
|
|
|
|
|
|
|
|
# for making requests
|
|
|
|
# backward compatibility with python2
|
|
|
|
cafile = None
|
|
|
|
if sys.version[0] == "2":
|
|
|
|
from urllib import urlencode
|
|
|
|
from urllib2 import urlopen, Request
|
|
|
|
else:
|
|
|
|
from urllib.parse import urlencode
|
|
|
|
from urllib.request import urlopen, Request
|
2020-08-16 00:00:08 +02:00
|
|
|
|
2020-08-12 03:24:23 +02:00
|
|
|
try:
|
|
|
|
import certifi
|
2020-08-16 00:00:08 +02:00
|
|
|
|
2020-08-12 03:24:23 +02:00
|
|
|
cafile = certifi.where()
|
|
|
|
except ImportError:
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
# --------------------------------------------
|
|
|
|
# Class containing the endpoint URLs for Toggl
|
|
|
|
# --------------------------------------------
|
2020-08-16 00:00:08 +02:00
|
|
|
class Endpoints:
|
2021-07-03 22:43:38 +02:00
|
|
|
WORKSPACES = "https://api.track.toggl.com/api/v8/workspaces"
|
|
|
|
CLIENTS = "https://api.track.toggl.com/api/v8/clients"
|
|
|
|
PROJECTS = "https://api.track.toggl.com/api/v8/projects"
|
|
|
|
TASKS = "https://api.track.toggl.com/api/v8/tasks"
|
2020-08-12 03:24:23 +02:00
|
|
|
REPORT_WEEKLY = "https://toggl.com/reports/api/v2/weekly"
|
|
|
|
REPORT_DETAILED = "https://toggl.com/reports/api/v2/details"
|
|
|
|
REPORT_SUMMARY = "https://toggl.com/reports/api/v2/summary"
|
2021-07-03 22:43:38 +02:00
|
|
|
START_TIME = "https://api.track.toggl.com/api/v8/time_entries/start"
|
|
|
|
TIME_ENTRIES = "https://api.track.toggl.com/api/v8/time_entries"
|
|
|
|
CURRENT_RUNNING_TIME = "https://api.track.toggl.com/api/v8/time_entries/current"
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def STOP_TIME(pid):
|
2021-07-03 22:43:38 +02:00
|
|
|
return "https://api.track.toggl.com/api/v8/time_entries/" + str(pid) + "/stop"
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------
|
|
|
|
# Class containing the necessities for Toggl interaction
|
|
|
|
# ------------------------------------------------------
|
2020-08-16 00:00:08 +02:00
|
|
|
class Toggl:
|
2020-08-12 03:24:23 +02:00
|
|
|
# template of headers for our request
|
|
|
|
headers = {
|
|
|
|
"Authorization": "",
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
"Accept": "*/*",
|
|
|
|
"User-Agent": "python/urllib",
|
|
|
|
}
|
|
|
|
|
|
|
|
# default API user agent value
|
|
|
|
user_agent = "TogglPy"
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
# Auxiliary methods
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
|
|
|
|
def decodeJSON(self, jsonString):
|
|
|
|
return json.JSONDecoder().decode(jsonString)
|
|
|
|
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
# Methods that modify the headers to control our HTTP requests
|
|
|
|
# ------------------------------------------------------------
|
|
|
|
def setAPIKey(self, APIKey):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""set the API key in the request header"""
|
2020-08-12 03:24:23 +02:00
|
|
|
# craft the Authorization
|
|
|
|
authHeader = APIKey + ":" + "api_token"
|
2020-08-16 00:00:08 +02:00
|
|
|
authHeader = "Basic " + b64encode(authHeader.encode()).decode("ascii").rstrip()
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
# add it into the header
|
2020-08-16 00:00:08 +02:00
|
|
|
self.headers["Authorization"] = authHeader
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
def setAuthCredentials(self, email, password):
|
2020-08-16 00:00:08 +02:00
|
|
|
authHeader = "{0}:{1}".format(email, password)
|
|
|
|
authHeader = "Basic " + b64encode(authHeader.encode()).decode("ascii").rstrip()
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
# add it into the header
|
2020-08-16 00:00:08 +02:00
|
|
|
self.headers["Authorization"] = authHeader
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
def setUserAgent(self, agent):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""set the User-Agent setting, by default it's set to TogglPy"""
|
2020-08-12 03:24:23 +02:00
|
|
|
self.user_agent = agent
|
|
|
|
|
|
|
|
# -----------------------------------------------------
|
|
|
|
# Methods for directly requesting data from an endpoint
|
|
|
|
# -----------------------------------------------------
|
|
|
|
|
|
|
|
def requestRaw(self, endpoint, parameters=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""make a request to the toggle api at a certain endpoint and return the RAW page data (usually JSON)"""
|
2020-08-12 03:24:23 +02:00
|
|
|
if parameters is None:
|
2020-08-16 00:00:08 +02:00
|
|
|
return urlopen(
|
|
|
|
Request(endpoint, headers=self.headers), cafile=cafile
|
|
|
|
).read()
|
2020-08-12 03:24:23 +02:00
|
|
|
else:
|
2020-08-16 00:00:08 +02:00
|
|
|
if "user_agent" not in parameters:
|
|
|
|
parameters.update(
|
|
|
|
{"user_agent": self.user_agent}
|
|
|
|
) # add our class-level user agent in there
|
2020-08-12 03:24:23 +02:00
|
|
|
# encode all of our data for a get request & modify the URL
|
|
|
|
endpoint = endpoint + "?" + urlencode(parameters)
|
|
|
|
# make request and read the response
|
2020-08-16 00:00:08 +02:00
|
|
|
return urlopen(
|
|
|
|
Request(endpoint, headers=self.headers), cafile=cafile
|
|
|
|
).read()
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
def request(self, endpoint, parameters=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""make a request to the toggle api at a certain endpoint and return the page data as a parsed JSON dict"""
|
|
|
|
return json.loads(self.requestRaw(endpoint, parameters).decode("utf-8"))
|
|
|
|
|
|
|
|
def postRequest(self, endpoint, parameters=None, method="POST"):
|
|
|
|
"""make a POST request to the toggle api at a certain endpoint and return the RAW page data (usually JSON)"""
|
|
|
|
if (
|
|
|
|
method == "DELETE"
|
|
|
|
): # Calls to the API using the DELETE mothod return a HTTP response rather than JSON
|
|
|
|
return urlopen(
|
|
|
|
Request(endpoint, headers=self.headers, method=method), cafile=cafile
|
|
|
|
).code
|
2020-08-12 03:24:23 +02:00
|
|
|
if parameters is None:
|
2020-08-16 00:00:08 +02:00
|
|
|
return (
|
|
|
|
urlopen(
|
|
|
|
Request(endpoint, headers=self.headers, method=method),
|
|
|
|
cafile=cafile,
|
|
|
|
)
|
|
|
|
.read()
|
|
|
|
.decode("utf-8")
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
else:
|
|
|
|
data = json.JSONEncoder().encode(parameters)
|
2020-08-16 00:00:08 +02:00
|
|
|
binary_data = data.encode("utf-8")
|
2020-08-12 03:24:23 +02:00
|
|
|
# make request and read the response
|
2020-08-16 00:00:08 +02:00
|
|
|
return (
|
|
|
|
urlopen(
|
|
|
|
Request(
|
|
|
|
endpoint, data=binary_data, headers=self.headers, method=method
|
|
|
|
),
|
|
|
|
cafile=cafile,
|
|
|
|
)
|
|
|
|
.read()
|
|
|
|
.decode("utf-8")
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
# ---------------------------------
|
|
|
|
# Methods for managing Time Entries
|
|
|
|
# ---------------------------------
|
|
|
|
|
|
|
|
def startTimeEntry(self, description, pid=None, tid=None, tags=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""starts a new Time Entry"""
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
data = {
|
2020-08-16 00:00:08 +02:00
|
|
|
"time_entry": {"created_with": self.user_agent, "description": description}
|
2020-08-12 03:24:23 +02:00
|
|
|
}
|
|
|
|
if pid:
|
|
|
|
data["time_entry"]["pid"] = pid
|
|
|
|
|
|
|
|
if tid:
|
|
|
|
data["time_entry"]["tid"] = tid
|
|
|
|
if tags:
|
|
|
|
data["time_entry"]["tags"] = tags
|
|
|
|
|
|
|
|
response = self.postRequest(Endpoints.START_TIME, parameters=data)
|
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
|
|
|
def currentRunningTimeEntry(self):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""Gets the Current Time Entry"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.CURRENT_RUNNING_TIME)
|
|
|
|
|
|
|
|
def stopTimeEntry(self, entryid):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""Stop the time entry"""
|
|
|
|
response = self.postRequest(Endpoints.STOP_TIME(entryid), method="PUT")
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
def createTimeEntry(
|
|
|
|
self,
|
|
|
|
hourduration,
|
|
|
|
description=None,
|
|
|
|
projectid=None,
|
|
|
|
projectname=None,
|
|
|
|
taskid=None,
|
|
|
|
clientname=None,
|
|
|
|
year=None,
|
|
|
|
month=None,
|
|
|
|
day=None,
|
|
|
|
hour=None,
|
|
|
|
billable=False,
|
|
|
|
hourdiff=-2,
|
|
|
|
):
|
2020-08-12 03:24:23 +02:00
|
|
|
"""
|
|
|
|
Creating a custom time entry, minimum must is hour duration and project param
|
|
|
|
:param hourduration:
|
|
|
|
:param description: Sets a descripton for the newly created time entry
|
|
|
|
:param projectid: Not required if projectname given
|
|
|
|
:param projectname: Not required if projectid was given
|
|
|
|
:param taskid: Adds a task to the time entry (Requirement: Toggl Starter or higher)
|
|
|
|
:param clientname: Can speed up project query process
|
|
|
|
:param year: Taken from now() if not provided
|
|
|
|
:param month: Taken from now() if not provided
|
|
|
|
:param day: Taken from now() if not provided
|
|
|
|
:param hour: Taken from now() if not provided
|
|
|
|
:return: response object from post call
|
|
|
|
"""
|
2020-08-16 00:00:08 +02:00
|
|
|
data = {"time_entry": {}}
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
if not projectid:
|
|
|
|
if projectname and clientname:
|
2020-08-16 00:00:08 +02:00
|
|
|
projectid = (self.getClientProject(clientname, projectname))["data"][
|
|
|
|
"id"
|
|
|
|
]
|
2020-08-12 03:24:23 +02:00
|
|
|
elif projectname:
|
2020-08-16 00:00:08 +02:00
|
|
|
projectid = (self.searchClientProject(projectname))["data"]["id"]
|
2020-08-12 03:24:23 +02:00
|
|
|
else:
|
2020-08-16 00:00:08 +02:00
|
|
|
print("Too many missing parameters for query")
|
2020-08-12 03:24:23 +02:00
|
|
|
exit(1)
|
|
|
|
|
|
|
|
if description:
|
2020-08-16 00:00:08 +02:00
|
|
|
data["time_entry"]["description"] = description
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
if taskid:
|
2020-08-16 00:00:08 +02:00
|
|
|
data["time_entry"]["tid"] = taskid
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
year = datetime.now().year if not year else year
|
|
|
|
month = datetime.now().month if not month else month
|
|
|
|
day = datetime.now().day if not day else day
|
|
|
|
hour = datetime.now().hour if not hour else hour
|
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
timestruct = datetime(year, month, day, hour + hourdiff).isoformat() + ".000Z"
|
|
|
|
data["time_entry"]["start"] = timestruct
|
|
|
|
data["time_entry"]["duration"] = hourduration * 3600
|
|
|
|
data["time_entry"]["pid"] = projectid
|
|
|
|
data["time_entry"]["created_with"] = "NAME"
|
|
|
|
data["time_entry"]["billable"] = billable
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
response = self.postRequest(Endpoints.TIME_ENTRIES, parameters=data)
|
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
|
|
|
def putTimeEntry(self, parameters):
|
2020-08-16 00:00:08 +02:00
|
|
|
if "id" not in parameters:
|
2020-08-12 03:24:23 +02:00
|
|
|
raise Exception("An id must be provided in order to put a time entry")
|
2020-08-16 00:00:08 +02:00
|
|
|
id = parameters["id"]
|
2020-08-12 03:24:23 +02:00
|
|
|
if type(id) is not int:
|
|
|
|
raise Exception("Invalid id %s provided " % (id))
|
2020-08-16 00:00:08 +02:00
|
|
|
endpoint = (
|
|
|
|
Endpoints.TIME_ENTRIES + "/" + str(id)
|
|
|
|
) # encode all of our data for a put request & modify the URL
|
|
|
|
data = json.JSONEncoder().encode({"time_entry": parameters})
|
2020-08-12 03:24:23 +02:00
|
|
|
request = Request(endpoint, data=data, headers=self.headers)
|
|
|
|
request.get_method = lambda: "PUT"
|
|
|
|
|
|
|
|
return json.loads(urlopen(request).read())
|
|
|
|
|
|
|
|
def deleteTimeEntry(self, entryid):
|
|
|
|
"""
|
|
|
|
Delete the specified timeEntry
|
|
|
|
:param entryid: The id of the entry to delete
|
|
|
|
"""
|
2020-08-16 00:00:08 +02:00
|
|
|
response = self.postRequest(
|
|
|
|
Endpoints.TIME_ENTRIES + "/{0}".format(entryid), method="DELETE"
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return response
|
|
|
|
|
|
|
|
# ----------------------------------
|
|
|
|
# Methods for getting workspace data
|
|
|
|
# ----------------------------------
|
|
|
|
def getWorkspaces(self):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return all the workspaces for a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.WORKSPACES)
|
|
|
|
|
|
|
|
def getWorkspace(self, name=None, id=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return the first workspace that matches a given name or id"""
|
2020-08-12 03:24:23 +02:00
|
|
|
workspaces = self.getWorkspaces() # get all workspaces
|
|
|
|
|
|
|
|
# if they give us nothing let them know we're not returning anything
|
|
|
|
if name is None and id is None:
|
2020-08-16 00:00:08 +02:00
|
|
|
print(
|
|
|
|
"Error in getWorkspace(), please enter either a name or an id as a filter"
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
if id is None: # then we search by name
|
2020-08-16 00:00:08 +02:00
|
|
|
for (
|
|
|
|
workspace
|
|
|
|
) in workspaces: # search through them for one matching the name provided
|
|
|
|
if workspace["name"] == name:
|
2020-08-12 03:24:23 +02:00
|
|
|
return workspace # if we find it return it
|
|
|
|
return None # if we get to here and haven't found it return None
|
|
|
|
else: # otherwise search by id
|
2020-08-16 00:00:08 +02:00
|
|
|
for (
|
|
|
|
workspace
|
|
|
|
) in workspaces: # search through them for one matching the id provided
|
|
|
|
if workspace["id"] == int(id):
|
2020-08-12 03:24:23 +02:00
|
|
|
return workspace # if we find it return it
|
|
|
|
return None # if we get to here and haven't found it return None
|
|
|
|
|
|
|
|
def getWorkspaceProjects(self, id):
|
|
|
|
"""
|
|
|
|
Return all of the projects for a given Workspace
|
|
|
|
:param id: Workspace ID by which to query
|
|
|
|
:return: Projects object returned from endpoint
|
|
|
|
"""
|
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
return self.request(Endpoints.WORKSPACES + "/{0}".format(id) + "/projects")
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
# -------------------------------
|
|
|
|
# Methods for getting client data
|
|
|
|
# -------------------------------
|
|
|
|
|
|
|
|
def getClients(self):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return all clients that are visable to a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.CLIENTS)
|
|
|
|
|
|
|
|
def getClient(self, name=None, id=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return the first workspace that matches a given name or id"""
|
2020-08-12 03:24:23 +02:00
|
|
|
clients = self.getClients() # get all clients
|
|
|
|
|
|
|
|
# if they give us nothing let them know we're not returning anything
|
|
|
|
if name is None and id is None:
|
2020-08-16 00:00:08 +02:00
|
|
|
print(
|
|
|
|
"Error in getClient(), please enter either a name or an id as a filter"
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
if id is None: # then we search by name
|
2020-08-16 00:00:08 +02:00
|
|
|
for (
|
|
|
|
client
|
|
|
|
) in clients: # search through them for one matching the name provided
|
|
|
|
if client["name"] == name:
|
2020-08-12 03:24:23 +02:00
|
|
|
return client # if we find it return it
|
|
|
|
return None # if we get to here and haven't found it return None
|
|
|
|
else: # otherwise search by id
|
2020-08-16 00:00:08 +02:00
|
|
|
for (
|
|
|
|
client
|
|
|
|
) in clients: # search through them for one matching the id provided
|
|
|
|
if client["id"] == int(id):
|
2020-08-12 03:24:23 +02:00
|
|
|
return client # if we find it return it
|
|
|
|
return None # if we get to here and haven't found it return None
|
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
def getClientProjects(self, id, active="true"):
|
2020-08-12 03:24:23 +02:00
|
|
|
"""
|
|
|
|
:param id: Client ID by which to query
|
|
|
|
:param active: possible values true/false/both. By default true. If false, only archived projects are returned.
|
|
|
|
:return: Projects object returned from endpoint
|
|
|
|
"""
|
2020-08-16 00:00:08 +02:00
|
|
|
return self.request(
|
|
|
|
Endpoints.CLIENTS + "/{0}/projects?active={1}".format(id, active)
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
def searchClientProject(self, name):
|
|
|
|
"""
|
|
|
|
Provide only a projects name for query and search through entire available names
|
|
|
|
WARNING: Takes a long time!
|
|
|
|
If client name is known, 'getClientProject' would be advised
|
|
|
|
:param name: Desired Project's name
|
|
|
|
:return: Project object
|
|
|
|
"""
|
|
|
|
for client in self.getClients():
|
|
|
|
try:
|
2020-08-16 00:00:08 +02:00
|
|
|
for project in self.getClientProjects(client["id"]):
|
|
|
|
if project["name"] == name:
|
2020-08-12 03:24:23 +02:00
|
|
|
return project
|
|
|
|
except Exception:
|
|
|
|
continue
|
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
print("Could not find client by the name")
|
2020-08-12 03:24:23 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
def getClientProject(self, clientName, projectName):
|
|
|
|
"""
|
|
|
|
Fast query given the Client's name and Project's name
|
|
|
|
:param clientName:
|
|
|
|
:param projectName:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
for client in self.getClients():
|
2020-08-16 00:00:08 +02:00
|
|
|
if client["name"] == clientName:
|
|
|
|
cid = client["id"]
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
if not cid:
|
2020-08-16 00:00:08 +02:00
|
|
|
print("Could not find such client name")
|
2020-08-12 03:24:23 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
for projct in self.getClientProjects(cid):
|
2020-08-16 00:00:08 +02:00
|
|
|
if projct["name"] == projectName:
|
|
|
|
pid = projct["id"]
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
if not pid:
|
2020-08-16 00:00:08 +02:00
|
|
|
print("Could not find such project name")
|
2020-08-12 03:24:23 +02:00
|
|
|
return None
|
|
|
|
|
|
|
|
return self.getProject(pid)
|
|
|
|
|
|
|
|
# --------------------------------
|
|
|
|
# Methods for getting PROJECTS data
|
|
|
|
# --------------------------------
|
|
|
|
def getProject(self, pid):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return all projects that are visable to a user"""
|
|
|
|
return self.request(Endpoints.PROJECTS + "/{0}".format(pid))
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
def getProjectTasks(self, pid, archived=False):
|
|
|
|
"""
|
|
|
|
return all tasks of a given project
|
|
|
|
:param pid: Project ID
|
|
|
|
:param archived: choose wether to fetch archived tasks or not
|
|
|
|
"""
|
2020-08-16 00:00:08 +02:00
|
|
|
return self.request(Endpoints.PROJECTS + "/{0}".format(pid) + "/tasks")
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
# --------------------------------
|
|
|
|
# Methods for interacting with TASKS data
|
|
|
|
# --------------------------------
|
|
|
|
|
|
|
|
def createTask(self, name, pid, active=True, estimatedSeconds=False):
|
|
|
|
"""
|
|
|
|
create a new task (Requirement: Toggl Starter or higher)
|
|
|
|
:param name: Name of the task
|
|
|
|
:param pid: Project ID
|
|
|
|
:param active: Defines if the task is active or archived, default: active
|
|
|
|
:param estimatedSeconds: Estimation for the task in seconds
|
|
|
|
"""
|
|
|
|
|
|
|
|
data = {}
|
2020-08-16 00:00:08 +02:00
|
|
|
data["task"] = {}
|
|
|
|
data["task"]["name"] = name
|
|
|
|
data["task"]["pid"] = pid
|
|
|
|
data["task"]["active"] = active
|
|
|
|
data["task"]["estimated_seconds"] = estimatedSeconds
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
response = self.postRequest(Endpoints.TASKS, parameters=data)
|
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
|
|
|
# --------------------------------
|
|
|
|
# Methods for getting reports data
|
|
|
|
# ---------------------------------
|
|
|
|
def getWeeklyReport(self, data):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return a weekly report for a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.REPORT_WEEKLY, parameters=data)
|
|
|
|
|
|
|
|
def getWeeklyReportPDF(self, data, filename):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""save a weekly report as a PDF"""
|
2020-08-12 03:24:23 +02:00
|
|
|
# get the raw pdf file data
|
|
|
|
filedata = self.requestRaw(Endpoints.REPORT_WEEKLY + ".pdf", parameters=data)
|
|
|
|
|
|
|
|
# write the data to a file
|
|
|
|
with open(filename, "wb") as pdf:
|
|
|
|
pdf.write(filedata)
|
|
|
|
|
|
|
|
def getDetailedReport(self, data):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return a detailed report for a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.REPORT_DETAILED, parameters=data)
|
|
|
|
|
|
|
|
def getDetailedReportPages(self, data):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return detailed report data from all pages for a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
pages_index = 1
|
2020-08-16 00:00:08 +02:00
|
|
|
data["page"] = pages_index
|
2020-08-12 03:24:23 +02:00
|
|
|
pages = self.request(Endpoints.REPORT_DETAILED, parameters=data)
|
|
|
|
try:
|
2020-08-16 00:00:08 +02:00
|
|
|
pages_number = math.ceil(
|
|
|
|
pages.get("total_count", 0) / pages.get("per_page", 0)
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
except ZeroDivisionError:
|
|
|
|
pages_number = 0
|
|
|
|
for pages_index in range(2, pages_number + 1):
|
2020-08-16 00:00:08 +02:00
|
|
|
time.sleep(
|
|
|
|
1
|
|
|
|
) # There is rate limiting of 1 request per second (per IP per API token).
|
|
|
|
data["page"] = pages_index
|
|
|
|
pages["data"].extend(
|
|
|
|
self.request(Endpoints.REPORT_DETAILED, parameters=data).get("data", [])
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return pages
|
|
|
|
|
|
|
|
def getDetailedReportPDF(self, data, filename):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""save a detailed report as a pdf"""
|
2020-08-12 03:24:23 +02:00
|
|
|
# get the raw pdf file data
|
|
|
|
filedata = self.requestRaw(Endpoints.REPORT_DETAILED + ".pdf", parameters=data)
|
|
|
|
|
|
|
|
# write the data to a file
|
|
|
|
with open(filename, "wb") as pdf:
|
|
|
|
pdf.write(filedata)
|
|
|
|
|
|
|
|
def getDetailedReportCSV(self, data, filename=None):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""save a detailed report as a csv"""
|
2020-08-12 03:24:23 +02:00
|
|
|
# get the raw pdf file data
|
|
|
|
filedata = self.requestRaw(Endpoints.REPORT_DETAILED + ".csv", parameters=data)
|
|
|
|
|
|
|
|
if filename:
|
|
|
|
# write the data to a file
|
|
|
|
with open(filename, "wb") as pdf:
|
|
|
|
pdf.write(filedata)
|
|
|
|
else:
|
|
|
|
return filedata
|
|
|
|
|
|
|
|
def getSummaryReport(self, data):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""return a summary report for a user"""
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.request(Endpoints.REPORT_SUMMARY, parameters=data)
|
|
|
|
|
|
|
|
def getSummaryReportPDF(self, data, filename):
|
2020-08-16 00:00:08 +02:00
|
|
|
"""save a summary report as a pdf"""
|
2020-08-12 03:24:23 +02:00
|
|
|
# get the raw pdf file data
|
|
|
|
filedata = self.requestRaw(Endpoints.REPORT_SUMMARY + ".pdf", parameters=data)
|
|
|
|
|
|
|
|
# write the data to a file
|
|
|
|
with open(filename, "wb") as pdf:
|
|
|
|
pdf.write(filedata)
|
|
|
|
|
|
|
|
# --------------------------------
|
|
|
|
# Methods for creating, updating, and deleting clients
|
|
|
|
# ---------------------------------
|
|
|
|
def createClient(self, name, wid, notes=None):
|
|
|
|
"""
|
|
|
|
create a new client
|
|
|
|
:param name: Name the client
|
|
|
|
:param wid: Workspace ID
|
|
|
|
:param notes: Notes for the client (optional)
|
|
|
|
"""
|
|
|
|
|
|
|
|
data = {}
|
2020-08-16 00:00:08 +02:00
|
|
|
data["client"] = {}
|
|
|
|
data["client"]["name"] = name
|
|
|
|
data["client"]["wid"] = wid
|
|
|
|
data["client"]["notes"] = notes
|
2020-08-12 03:24:23 +02:00
|
|
|
|
|
|
|
response = self.postRequest(Endpoints.CLIENTS, parameters=data)
|
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
|
|
|
def updateClient(self, id, name=None, notes=None):
|
|
|
|
"""
|
|
|
|
Update data for an existing client. If the name or notes parameter is not supplied, the existing data on the Toggl server will not be changed.
|
|
|
|
:param id: The id of the client to update
|
|
|
|
:param name: Update the name of the client (optional)
|
|
|
|
:param notes: Update the notes for the client (optional)
|
|
|
|
"""
|
|
|
|
|
|
|
|
data = {}
|
2020-08-16 00:00:08 +02:00
|
|
|
data["client"] = {}
|
|
|
|
data["client"]["name"] = name
|
|
|
|
data["client"]["notes"] = notes
|
2020-08-12 03:24:23 +02:00
|
|
|
|
2020-08-16 00:00:08 +02:00
|
|
|
response = self.postRequest(
|
|
|
|
Endpoints.CLIENTS + "/{0}".format(id), parameters=data, method="PUT"
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return self.decodeJSON(response)
|
|
|
|
|
|
|
|
def deleteClient(self, id):
|
|
|
|
"""
|
|
|
|
Delete the specified client
|
|
|
|
:param id: The id of the client to delete
|
|
|
|
"""
|
2020-08-16 00:00:08 +02:00
|
|
|
response = self.postRequest(
|
|
|
|
Endpoints.CLIENTS + "/{0}".format(id), method="DELETE"
|
|
|
|
)
|
2020-08-12 03:24:23 +02:00
|
|
|
return response
|