Merge develop

This commit is contained in:
Roy Nieterau 2023-03-29 14:46:41 +02:00
parent 6405700ed9
commit 669a2256ef
248 changed files with 13531 additions and 8058 deletions

View file

@ -6,34 +6,22 @@ import datetime
import requests
from .constants import (
CLOCKIFY_ENDPOINT,
ADMIN_PERMISSION_NAMES
ADMIN_PERMISSION_NAMES,
)
from openpype.lib.local_settings import OpenPypeSecureRegistry
def time_check(obj):
if obj.request_counter < 10:
obj.request_counter += 1
return
wait_time = 1 - (time.time() - obj.request_time)
if wait_time > 0:
time.sleep(wait_time)
obj.request_time = time.time()
obj.request_counter = 0
from openpype.lib import Logger
class ClockifyAPI:
log = Logger.get_logger(__name__)
def __init__(self, api_key=None, master_parent=None):
self.workspace_name = None
self.workspace_id = None
self.master_parent = master_parent
self.api_key = api_key
self.request_counter = 0
self.request_time = time.time()
self._workspace_id = None
self._user_id = None
self._secure_registry = None
@property
@ -44,11 +32,19 @@ class ClockifyAPI:
@property
def headers(self):
return {"X-Api-Key": self.api_key}
return {"x-api-key": self.api_key}
@property
def workspace_id(self):
return self._workspace_id
@property
def user_id(self):
return self._user_id
def verify_api(self):
for key, value in self.headers.items():
if value is None or value.strip() == '':
if value is None or value.strip() == "":
return False
return True
@ -59,65 +55,55 @@ class ClockifyAPI:
if api_key is not None and self.validate_api_key(api_key) is True:
self.api_key = api_key
self.set_workspace()
self.set_user_id()
if self.master_parent:
self.master_parent.signed_in()
return True
return False
def validate_api_key(self, api_key):
test_headers = {'X-Api-Key': api_key}
action_url = 'workspaces/'
time_check(self)
test_headers = {"x-api-key": api_key}
action_url = "user"
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=test_headers
CLOCKIFY_ENDPOINT + action_url, headers=test_headers
)
if response.status_code != 200:
return False
return True
def validate_workspace_perm(self, workspace_id=None):
user_id = self.get_user_id()
def validate_workspace_permissions(self, workspace_id=None, user_id=None):
if user_id is None:
self.log.info("No user_id found during validation")
return False
if workspace_id is None:
workspace_id = self.workspace_id
action_url = "/workspaces/{}/users/{}/permissions".format(
workspace_id, user_id
)
time_check(self)
action_url = f"workspaces/{workspace_id}/users?includeRoles=1"
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
user_permissions = response.json()
for perm in user_permissions:
if perm['name'] in ADMIN_PERMISSION_NAMES:
data = response.json()
for user in data:
if user.get("id") == user_id:
roles_data = user.get("roles")
for entities in roles_data:
if entities.get("role") in ADMIN_PERMISSION_NAMES:
return True
return False
def get_user_id(self):
action_url = 'v1/user/'
time_check(self)
action_url = "user"
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
# this regex is neccessary: UNICODE strings are crashing
# during json serialization
id_regex = '\"{1}id\"{1}\:{1}\"{1}\w+\"{1}'
result = re.findall(id_regex, str(response.content))
if len(result) != 1:
# replace with log and better message?
print('User ID was not found (this is a BUG!!!)')
return None
return json.loads('{'+result[0]+'}')['id']
result = response.json()
user_id = result.get("id", None)
return user_id
def set_workspace(self, name=None):
if name is None:
name = os.environ.get('CLOCKIFY_WORKSPACE', None)
name = os.environ.get("CLOCKIFY_WORKSPACE", None)
self.workspace_name = name
self.workspace_id = None
if self.workspace_name is None:
return
try:
@ -125,7 +111,7 @@ class ClockifyAPI:
except Exception:
result = False
if result is not False:
self.workspace_id = result
self._workspace_id = result
if self.master_parent is not None:
self.master_parent.start_timer_check()
return True
@ -139,6 +125,14 @@ class ClockifyAPI:
return all_workspaces[name]
return False
def set_user_id(self):
try:
user_id = self.get_user_id()
except Exception:
user_id = None
if user_id is not None:
self._user_id = user_id
def get_api_key(self):
return self.secure_registry.get_item("api_key", None)
@ -146,11 +140,9 @@ class ClockifyAPI:
self.secure_registry.set_item("api_key", api_key)
def get_workspaces(self):
action_url = 'workspaces/'
time_check(self)
action_url = "workspaces/"
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return {
workspace["name"]: workspace["id"] for workspace in response.json()
@ -159,27 +151,22 @@ class ClockifyAPI:
def get_projects(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/projects/'.format(workspace_id)
time_check(self)
action_url = f"workspaces/{workspace_id}/projects"
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return {
project["name"]: project["id"] for project in response.json()
}
if response.status_code != 403:
result = response.json()
return {project["name"]: project["id"] for project in result}
def get_project_by_id(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/projects/{}/'.format(
action_url = "workspaces/{}/projects/{}".format(
workspace_id, project_id
)
time_check(self)
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()
@ -187,32 +174,24 @@ class ClockifyAPI:
def get_tags(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/tags/'.format(workspace_id)
time_check(self)
action_url = "workspaces/{}/tags".format(workspace_id)
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return {
tag["name"]: tag["id"] for tag in response.json()
}
return {tag["name"]: tag["id"] for tag in response.json()}
def get_tasks(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/projects/{}/tasks/'.format(
action_url = "workspaces/{}/projects/{}/tasks".format(
workspace_id, project_id
)
time_check(self)
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return {
task["name"]: task["id"] for task in response.json()
}
return {task["name"]: task["id"] for task in response.json()}
def get_workspace_id(self, workspace_name):
all_workspaces = self.get_workspaces()
@ -236,48 +215,64 @@ class ClockifyAPI:
return None
return all_tasks[tag_name]
def get_task_id(
self, task_name, project_id, workspace_id=None
):
def get_task_id(self, task_name, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
all_tasks = self.get_tasks(
project_id, workspace_id
)
all_tasks = self.get_tasks(project_id, workspace_id)
if task_name not in all_tasks:
return None
return all_tasks[task_name]
def get_current_time(self):
return str(datetime.datetime.utcnow().isoformat())+'Z'
return str(datetime.datetime.utcnow().isoformat()) + "Z"
def start_time_entry(
self, description, project_id, task_id=None, tag_ids=[],
workspace_id=None, billable=True
self,
description,
project_id,
task_id=None,
tag_ids=None,
workspace_id=None,
user_id=None,
billable=True,
):
# Workspace
if workspace_id is None:
workspace_id = self.workspace_id
# User ID
if user_id is None:
user_id = self._user_id
# get running timer to check if we need to start it
current_timer = self.get_in_progress()
# Check if is currently run another times and has same values
current = self.get_in_progress(workspace_id)
if current is not None:
# DO not restart the timer, if it is already running for curent task
if current_timer:
current_timer_hierarchy = current_timer.get("description")
current_project_id = current_timer.get("projectId")
current_task_id = current_timer.get("taskId")
if (
current.get("description", None) == description and
current.get("projectId", None) == project_id and
current.get("taskId", None) == task_id
description == current_timer_hierarchy
and project_id == current_project_id
and task_id == current_task_id
):
self.log.info(
"Timer for the current project is already running"
)
self.bool_timer_run = True
return self.bool_timer_run
self.finish_time_entry(workspace_id)
self.finish_time_entry()
# Convert billable to strings
if billable:
billable = 'true'
billable = "true"
else:
billable = 'false'
billable = "false"
# Rest API Action
action_url = 'workspaces/{}/timeEntries/'.format(workspace_id)
action_url = "workspaces/{}/user/{}/time-entries".format(
workspace_id, user_id
)
start = self.get_current_time()
body = {
"start": start,
@ -285,169 +280,135 @@ class ClockifyAPI:
"description": description,
"projectId": project_id,
"taskId": task_id,
"tagIds": tag_ids
"tagIds": tag_ids,
}
time_check(self)
response = requests.post(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
success = False
if response.status_code < 300:
success = True
return success
return True
return False
def get_in_progress(self, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/timeEntries/inProgress'.format(
workspace_id
)
time_check(self)
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
)
def _get_current_timer_values(self, response):
if response is None:
return
try:
output = response.json()
except json.decoder.JSONDecodeError:
output = None
return output
return None
if output and isinstance(output, list):
return output[0]
return None
def finish_time_entry(self, workspace_id=None):
def get_in_progress(self, user_id=None, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
current = self.get_in_progress(workspace_id)
if current is None:
return
if user_id is None:
user_id = self.user_id
current_id = current["id"]
action_url = 'workspaces/{}/timeEntries/{}'.format(
workspace_id, current_id
action_url = (
f"workspaces/{workspace_id}/user/"
f"{user_id}/time-entries?in-progress=1"
)
body = {
"start": current["timeInterval"]["start"],
"billable": current["billable"],
"description": current["description"],
"projectId": current["projectId"],
"taskId": current["taskId"],
"tagIds": current["tagIds"],
"end": self.get_current_time()
}
time_check(self)
response = requests.put(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
response = requests.get(
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return self._get_current_timer_values(response)
def finish_time_entry(self, workspace_id=None, user_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
if user_id is None:
user_id = self.user_id
current_timer = self.get_in_progress()
if not current_timer:
return
action_url = "workspaces/{}/user/{}/time-entries".format(
workspace_id, user_id
)
body = {"end": self.get_current_time()}
response = requests.patch(
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def get_time_entries(
self, workspace_id=None, quantity=10
):
def get_time_entries(self, workspace_id=None, user_id=None, quantity=10):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/timeEntries/'.format(workspace_id)
time_check(self)
if user_id is None:
user_id = self.user_id
action_url = "workspaces/{}/user/{}/time-entries".format(
workspace_id, user_id
)
response = requests.get(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()[:quantity]
def remove_time_entry(self, tid, workspace_id=None):
def remove_time_entry(self, tid, workspace_id=None, user_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/timeEntries/{}'.format(
workspace_id, tid
action_url = "workspaces/{}/user/{}/time-entries/{}".format(
workspace_id, user_id, tid
)
time_check(self)
response = requests.delete(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers
CLOCKIFY_ENDPOINT + action_url, headers=self.headers
)
return response.json()
def add_project(self, name, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/projects/'.format(workspace_id)
action_url = "workspaces/{}/projects".format(workspace_id)
body = {
"name": name,
"clientId": "",
"isPublic": "false",
"estimate": {
"estimate": 0,
"type": "AUTO"
},
"estimate": {"estimate": 0, "type": "AUTO"},
"color": "#f44336",
"billable": "true"
"billable": "true",
}
time_check(self)
response = requests.post(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def add_workspace(self, name):
action_url = 'workspaces/'
action_url = "workspaces/"
body = {"name": name}
time_check(self)
response = requests.post(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def add_task(
self, name, project_id, workspace_id=None
):
def add_task(self, name, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/projects/{}/tasks/'.format(
action_url = "workspaces/{}/projects/{}/tasks".format(
workspace_id, project_id
)
body = {
"name": name,
"projectId": project_id
}
time_check(self)
body = {"name": name, "projectId": project_id}
response = requests.post(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def add_tag(self, name, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = 'workspaces/{}/tags'.format(workspace_id)
body = {
"name": name
}
time_check(self)
action_url = "workspaces/{}/tags".format(workspace_id)
body = {"name": name}
response = requests.post(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
json=body
CLOCKIFY_ENDPOINT + action_url, headers=self.headers, json=body
)
return response.json()
def delete_project(
self, project_id, workspace_id=None
):
def delete_project(self, project_id, workspace_id=None):
if workspace_id is None:
workspace_id = self.workspace_id
action_url = '/workspaces/{}/projects/{}'.format(
action_url = "/workspaces/{}/projects/{}".format(
workspace_id, project_id
)
time_check(self)
response = requests.delete(
CLOCKIFY_ENDPOINT + action_url,
headers=self.headers,
@ -455,12 +416,12 @@ class ClockifyAPI:
return response.json()
def convert_input(
self, entity_id, entity_name, mode='Workspace', project_id=None
self, entity_id, entity_name, mode="Workspace", project_id=None
):
if entity_id is None:
error = False
error_msg = 'Missing information "{}"'
if mode.lower() == 'workspace':
if mode.lower() == "workspace":
if entity_id is None and entity_name is None:
if self.workspace_id is not None:
entity_id = self.workspace_id
@ -471,14 +432,14 @@ class ClockifyAPI:
else:
if entity_id is None and entity_name is None:
error = True
elif mode.lower() == 'project':
elif mode.lower() == "project":
entity_id = self.get_project_id(entity_name)
elif mode.lower() == 'task':
elif mode.lower() == "task":
entity_id = self.get_task_id(
task_name=entity_name, project_id=project_id
)
else:
raise TypeError('Unknown type')
raise TypeError("Unknown type")
# Raise error
if error:
raise ValueError(error_msg.format(mode))