import requests import json API_URL = "http://localhost:3000" API_USER = "demo@demo.demo" API_PASS = "demo" OFFSET = 65535 class Planka: """API wrapper class for Planka @url: URL of Planka instance @username: Username of Planka user @password: Password of Planka user """ def __init__(self, url:str, username:str, password:str, templates="config/templates.json"): self.url = url self.username = username self.password = password self.auth = None with open(templates) as f: self.templates = json.load(f) self.authenticate() def __repr__(self): return f"<{type(self).__name__}:\n\tBase URL: {self.url}\n\tLogin User: {self.username}\n\tLogin Pass: {self.password}\n\tAPI Token: {self.auth}\n>" def deauthenticate(self) -> bool: """Deletes the auth token from the Planka API @return: True if successful, False if not """ try: self.request("DELETE", "/api/access-tokens/me") self.auth = None return True except: raise InvalidToken(f"No active access token assigned to this instance\n{self.__repr__()}") def validate(self) -> bool: """Validates the Planka API connection @return: True if successful, False if not """ try: self.request("GET", "/*") return True except: raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}") def authenticate(self) -> bool: """Gets an auth token from the Planka API @return: True if successful, False if not """ try: request = requests.post(f"{self.url}/api/access-tokens", data={'emailOrUsername': self.username, 'password': self.password}) self.auth = request.json()['item'] if not self.auth: raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}") return True except: raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}") def request(self, method:str, endpoint:str, data:dict=None) -> dict: """Makes a request to the Planka API @method: HTTP method @endpoint: API endpoint @data: Data to send with request (default: None) @return: JSON response from Planka API """ if not self.auth: self.authenticate() headers = \ { "Content-Type": "application/json", "Authorization": f"Bearer {self.auth}" } url = f"{self.url}{endpoint}" response = requests.request(method, url, headers=headers, json=data) if response.status_code == 401: raise InvalidToken("Invalid API credentials") if response.status_code not in [200, 201]: raise InvalidToken(f"Failed to {method} {url} with status code {response.status_code}") try: return response.json() except: raise InvalidToken(f"Failed to parse response from {url}") def get_template(self, template:str) -> dict: """Returns a template from the templates.json file @template: Name of template to return @return: Template dictionary """ try: return self.templates[template] except: raise InvalidToken(f"Template not found: {template}") class Controller(): def __init__(self, instance:Planka) -> None: """Controller class for Planka API @instance: Planka API instance """ self.instance = instance self.template:dict = None self.data:dict = None self.response:dict = None def __str__(self) -> str: """Returns a string representation of the controller object @return: String representation of controller object """ return f"{type(self).__name__}:\n{json.dumps(self.data, sort_keys=True, indent=4)}" def __repr__(self) -> str: """Returns a string representation of the controller object @return: String representation of controller object """ return f"<{type(self).__name__}({self.__class__.__bases__[0].__name__})>{self.__str__()}" def build(self, **kwargs) -> dict: """Builds the controller data @return: Controller data dictionary """ if not kwargs: return kwargs valid_keys = self.template.keys() data = {key: value for key, value in kwargs.items() if key in valid_keys} self.data = data return self.data def create(self, route:str, data:dict=None) -> dict: """Creates a new controller object (POST) @route: Route for controller object POST request @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") self.response = self.instance.request("POST", route, data) return self.response def get(self, route:str) -> dict: """Gets a controller object (GET) @route: Route for controller object GET request @return: GET response dictionary """ return self.instance.request("GET", route) def update(self, route:str, data:dict=None) -> dict: """Updates a controller object (PATCH) @route: Route for controller object PATCH request @oid: ID of controller object @return: PATCH response dictionary """ if not data: data = self.data if not self.data: raise InvalidToken(f"Please Build a {type(self).__name__} before updating") self.response = self.instance.request("PATCH", route, data=data) return self.response def delete(self, route:str) -> dict: """Deletes a controller object (DELETE) @route: Route for controller object DELETE request @oid: ID of controller object @return: DELETE response dictionary """ return self.instance.request("DELETE", route) def last_response(self) -> dict: """Returns the last response from the controller object @return: Last response dictionary """ return self.response class Project(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("project") self.data = self.build(**kwargs) def get(self, name:str=None, oid:str=None) -> dict: """Gets a project by name @oid: ID of project to get (optional) @name: Name of project if None returns all projects @return: GET response dictionary """ if oid: return super().get(f"/api/projects/{oid}") prjs = super().get("/api/projects") if not name: return prjs prj_names = [prj["name"] for prj in prjs["items"]] if name not in prj_names: raise InvalidToken(f"Project {name} not found") prj_id = [prj for prj in prjs["items"] if prj["name"] == name][0]["id"] return super().get(f"/api/projects/{prj_id}") def get_project_names(self) -> list: """Gets a list of project names @return: List of project names """ return [prj["name"] for prj in self.get()['items']] def create(self) -> dict: """Creates a new project @return: POST response dictionary """ if not self.data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") if self.data["name"] in [prj["name"] for prj in self.get()['items']]: raise InvalidToken(f"Project {self.data['name']} already exists") return super().create("/api/projects") def update(self, name:str) -> dict: """Updates a project @name: Name of project to update @return: PATCH response dictionary """ prj_id = prj_id = self.get(name)['item']['id'] return super().update(f"/api/projects/{prj_id}") def delete(self, name:str) -> dict: """Deletes a project @name: Name of project to delete @return: DELETE response dictionary """ prj_id = self.get(name)['item']['id'] return super().delete(f"/api/projects/{prj_id}") class Board(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("board") self.data = self.build(**kwargs) def get(self, project_name:str=None, board_name:str=None, oid:str=None) -> dict: """Gets a board by name @oid: ID of board to get (optonal) @name: Name of board if None returns all boards @project_name: Name of project to get boards from @return: GET response dictionary """ if oid: return super().get(f"/api/boards/{oid}") if not (project_name): raise InvalidToken("Please provide a project name") prj_con = Project(self.instance) prj = prj_con.get(project_name) boards = prj["included"]["boards"] if not board_name: return boards board_names = [board["name"] for board in boards] if board_name not in board_names: raise InvalidToken(f"Board `{board_name}` not found") board_id = [board for board in boards if board["name"] == board_name][0]["id"] return super().get(f"/api/boards/{board_id}") def create(self, project_name:str) -> dict: """Creates a new board @prj_name: Name of project to create board in @return: POST response dictionary """ if not self.data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") prj_con = Project(self.instance) prj_id = prj_con.get(project_name)['item']['id'] return super().create(f"/api/projects/{prj_id}/boards") def update(self, project_name:str=None, board_name:str=None, data:dict=None, oid:str=None) -> dict: """Updates a board @oid: ID of board to update (optional) @project_name: Name of project to update board in @board_name: Name of board to update @return: PATCH response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before updating") if oid: return super().update(f"/api/boards/{oid}", data=data) if not (project_name and board_name): raise InvalidToken("Please provide project and board names") board_id = self.get(project_name, board_name)['item']['id'] return super().update(f"/api/boards/{board_id}", data=self.data) def delete(self, project_name:str=None, board_name:str=None, oid:str=None): """Deletes a board @oid: ID of board to delete (optional) @project_name: Name of project to delete board in @board_name: Name of board to delete @return: DELETE response dictionary """ if oid: return super().delete(f"/api/boards/{oid}") if not project_name: raise InvalidToken("Please provide a project name") if not board_name: raise InvalidToken("Please provide a board name") board_id = self.get(project_name, board_name)['item']['id'] return super().delete(f"/api/boards/{board_id}") class List(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("list") self.data = self.build(**kwargs) def get(self, project_name:str=None, board_name:str=None, list_name:str=None): """Gets a list by name NOTE: No GET route for list by ID @project_name: Name of project to get list from @board_name: Name of board to get list from @list_name: Name of list to get @return: GET response dictionary """ if not (project_name and board_name): raise InvalidToken("Please provide project and board names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) lists = board["included"]["lists"] list_names = [lst["name"] for lst in lists] if not list_name: return lists if list_name not in list_names: raise InvalidToken(f"List `{list_name}` not found") return [lst for lst in lists if lst["name"] == list_name][0] def create(self, project_name:str=None, board_name:str=None, data:dict=None): """Creates a new list @project_name: Name of project to create list in @board_name: Name of board to create list in @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") if not (project_name and board_name): raise InvalidToken("Please provide project and board name") board_con = Board(self.instance) board_id = board_con.get(project_name, board_name)['item']['id'] return super().create(f"/api/boards/{board_id}/lists") def update(self, project_name:str=None, board_name:str=None, list_name:str=None, data:dict=None, oid:str=None): """Updates a list @oid: ID of list to update (optional) @project_name: Name of project to update list in @board_name: Name of board to update list in @list_name: Name of list to update @return: PATCH response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before updating") if oid: return super().update(f"/api/lists/{oid}", data=data) if not (project_name and board_name and list_name): raise InvalidToken("Please provide project, board, and list names") lst = self.get(project_name, board_name, list_name) return super().update(f"/api/lists/{lst['id']}", data=data) def delete(self, project_name:str=None, board_name:str=None, list_name:str=None, oid:str=None): """Deletes a list @oid: ID of list to delete (optional) @project_name: Name of project to delete list in @board_name: Name of board to delete list in @list_name: Name of list to delete @return: DELETE response dictionary """ if oid: return super().delete(f"/api/lists/{oid}") if not (project_name and board_name and list_name): raise InvalidToken("Please provide a project, board, and list names") lst = self.get(project_name, board_name, list_name) return super().delete(f"/api/lists/{lst['id']}") class Card(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("card") self.data = self.build(**kwargs) def get(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, oid:str=None): """Gets a card by name @oid: ID of card to get (optional) @project_name: Name of project to get card from @board_name: Name of board to get card from @list_name: Name of list to get card from @card_name: Name of card to get @return: GET response dictionary """ if oid != None: return super().get(f"/api/cards/{oid}") if not (project_name and board_name and list_name): raise InvalidToken("Please provide project, board, and list names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) lst_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] cards = [card for card in board["included"]["cards"] if card["listId"] == lst_id] card_names = [card["name"] for card in cards] if not card_name: return [self.get(oid=card["id"]) for card in cards] if card_name not in card_names: raise InvalidToken(f"Card `{card_name}` not found") card_id = [card for card in cards if card["name"] == card_name][0]['id'] return super().get(f"/api/cards/{card_id}") def create(self, project_name:str=None, board_name:str=None, list_name:str=None, data:dict=None): """Creates a new card @project_name: Name of project to create card in @board_name: Name of board to create card in @list_name: Name of list to create card in @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") if not (project_name and board_name and list_name): raise InvalidToken("Please provide a project, board and list names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) lst_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] return super().create(f"/api/lists/{lst_id}/cards") def delete(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, oid:str=None): """Deletes a card @oid: ID of card to delete (optional) @project_name: Name of project to delete card in @board_name: Name of board to delete card in @list_name: Name of list to delete card in @card_name: Name of card to delete @return: DELETE response dictionary """ if oid != None: return super().delete(f"/api/cards/{oid}") if not (project_name and board_name and list_name and card_name): raise InvalidToken("Please provide a project, board, list, and card name") card = self.get(project_name, board_name, list_name, card_name) return super().delete(f"/api/cards/{card['id']}") def update(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, data:dict=None, oid:str=None): """Updates a card @oid: ID of card to update (optional) @project_name: Name of project to update card in @board_name: Name of board to update card in @list_name: Name of list to update card in @card_name: Name of card to update @return: PATCH response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before updating") if oid: return super().update(f"/api/cards/{oid}", data=data) if not (project_name and board_name and list_name and card_name): raise InvalidToken("Please provide a project, board, list, and card name") card = self.get(project_name, board_name, list_name, card_name) return super().update(f"/api/cards/{card['id']}", data=data) def get_labels(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, oid:str=None): """Gets labels for a card @oid: ID of card to get labels from (optional) @project_name: Name of project to get card from @board_name: Name of board to get card from @list_name: Name of list to get card from @card_name: Name of card to get @return: GET response dictionary """ if oid: return self.get(oid=oid)['included']['cardLabels'] if not (project_name and board_name and list_name and card_name): raise InvalidToken("Please provide project, board, list, and card names") card_id = self.get(project_name, board_name, list_name, card_name)['item']['id'] return self.get(oid=card_id)['included']['cardLabels'] class Label(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("label") self.options = instance.get_template("colors") self.data = self.build(**kwargs) def colors(self) -> list: return self.options def get(self, project_name:str=None, board_name:str=None, label_name:str=None) -> dict: """Gets a label by name @project_name: Name of project to get label from @board_name: Name of board to get label from @label_name: Name of label to get @return: GET response dictionary """ if not (project_name and board_name): raise InvalidToken("Please provide project and board names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) labels = board["included"]["labels"] label_names = [label["name"] for label in labels] if not label_name: return labels if label_name not in label_names: raise InvalidToken(f"Label `{label_name}` not found") return [label for label in labels if label["name"] == label_name][0] def create(self, project_name:str=None, board_name:str=None, data:dict=None): """Creates a new label @project_name: Name of project to create label in @board_name: Name of board to create label in @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") if not (project_name and board_name): raise InvalidToken("Please provide project and board names") board_con = Board(self.instance) board = board_con.get(project_name, board_name)['item'] return super().create(f"/api/boards/{board['id']}/labels") def delete(self, project_name:str=None, board_name:str=None, label_name:str=None, oid:str=None): """Deletes a label @oid: ID of label to delete (optional) @project_name: Name of project to delete label from @board_name: Name of board to delete label from @label_name: Name of label to delete @return: DELETE response dictionary """ if oid: return super().delete(f"/api/labels/{oid}") if not (project_name and board_name and label_name): raise InvalidToken("Please provide project, board, and label names") label = self.get(project_name, board_name, label_name) return super().delete(f"/api/labels/{label['id']}") def add(self, project_name:str=None, board_name:str=None, list_name:str=None ,card_name:str=None, label_name:str=None, card_id:str=None, label_id:str=None): """Adds a label to a card @project_name: Name of project to add label to card in @board_name: Name of board to add label to card in @label_name: Name of label to add to card @card_name: Name of card to add label to @list_name: Name of list to add label to card in @return: POST response dictionary """ if label_id and card_id: return super().create(f"/api/cards/{card_id}/labels", data={"labelId":label_id}) if not (project_name and board_name and label_name): raise InvalidToken("Please provide a project, board, label name") if card_id: label = self.get(project_name, board_name, label_name) return super().create(f"/api/cards/{card_id}/labels", data={"labelId":label['item']['id']}) if not (card_name and list_name): raise InvalidToken("Please provide a card and list name") card_con = Card(self.instance) card = card_con.get(project_name, board_name, list_name, card_name) label = self.get(project_name, board_name, label_name) return super().create(f"/api/cards/{card['item']['id']}/labels", {"labelId":label['item']['id']}) def remove(self, project_name:str=None, board_name:str=None, list_name:str=None ,card_name:str=None, label_name:str=None, card_id:str=None, label_id:str=None): """Removes a label from a card @project_name: Name of project to remove label from card in @board_name: Name of board to remove label from card in @label_name: Name of label to remove from card @card_name: Name of card to remove label from @list_name: Name of list to remove label from card in @return: DELETE response dictionary """ if label_id and card_id: return super().delete(f"/api/cards/{card_id}/labels/{label_id}") if not (project_name and board_name and label_name): raise InvalidToken("Please provide a project, board, label name") if card_id: label_id = [label['id'] for label in Card(self.instance).get_labels(oid=card_id) if label['name'] == label_name][0] return super().delete(f"/api/cards/{card_id}/labels/{label['item']['id']}") if not (card_name and list_name): raise InvalidToken("Please provide a card and list name") card_con = Card(self.instance) card = card_con.get(project_name, board_name, list_name, card_name) label = self.get(project_name, board_name, label_name) return super().delete(f"/api/cards/{card['item']['id']}/labels/{label['item']['id']}") class Task(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("task") self.data = self.build(**kwargs) def get(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, task_name:str=None) -> dict: """Gets a task by name NOTE: No GET route for tasks by OID @project_name: Name of project to get task from @board_name: Name of board to get task from @list_name: Name of list to get task from @card_name: Name of card to get task from @task_name: Name of task to get @return: GET response dictionary """ if not (project_name and board_name and list_name and card_name): raise InvalidToken("Please provide project, board, list, and card names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] cards = [card for card in board["included"]["cards"] if card["name"] == card_name and card["listId"] == list_id] card_id = [card for card in cards if card["name"] == card_name][0]["id"] tasks = [task for task in board["included"]["tasks"] if task["cardId"] == card_id] task_names = [task["name"] for task in tasks] if not task_name: return tasks if task_name not in task_names: raise InvalidToken(f"Task `{task_name}` not found") return [task for task in tasks if task["name"] == task_name][0] def create(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, data:dict=None, card_id:str=None): """Creates a new task @card_id: ID of card to create task in (optional) @project_name: Name of project to create task in @board_name: Name of board to create task in @list_name: Name of list to create task in @card_name: Name of card to create task in @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before creating") if card_id: return super().create(f"/api/cards/{card_id}/tasks") if not (project_name and board_name and list_name and card_name): raise InvalidToken("Please provide project, board, list, and card names") board_con = Board(self.instance) board = board_con.get(project_name, board_name) list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] cards = [card for card in board["included"]["cards"] if card["name"] == card_name and card["listId"] == list_id] card_id = [card for card in cards if card["name"] == card_name][0]["id"] return super().create(f"/api/cards/{card_id}/tasks") def update(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, task_name:str=None, data:dict=None, oid:str=None): """Updates a task @oid: Object ID of task to update (optional) @project_name: Name of project to update task in @board_name: Name of board to update task in @list_name: Name of list to update task in @card_name: Name of card to update task in @task_name: Name of task to update @return: PATCH response dictionary """ if not data: data = self.data if not data: raise InvalidToken(f"Please Build a {type(self).__name__} before updating") if oid: return super().update(f"/api/tasks/{oid}") if not (project_name and board_name and list_name and card_name and task_name): raise InvalidToken("Please provide project, board, list, card, and task names") task = self.get(project_name, board_name, list_name, card_name, task_name) return super().update(f"/api/tasks/{task['id']}") def delete(self, project_name:str=None, board_name:str=None, list_name:str=None, card_name:str=None, task_name:str=None, oid:str=None): """Deletes a task @oid: ID of task to delete (Use this if you already have the ID) @project_name: Name of project to delete task from @board_name: Name of board to delete task from @list_name: Name of list to delete task from @card_name: Name of card to delete task from @task_name: Name of task to delete @return: DELETE response dictionary """ if oid: return super().delete(f"/api/tasks/{id}") if not (project_name and board_name and list_name and card_name and task_name): raise InvalidToken("Please provide project, board, list, card, and task names") task = self.get(project_name, board_name, list_name, card_name, task_name) return super().delete(f"/api/tasks/{task['id']}") class Attachment(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("attachment") self.data = self.build(**kwargs) class Stopwatch(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("stopwatch") self.data = self.build(**kwargs) class Background(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("background") self.options = instance.get_template("gradients") self.data = self.build(**kwargs) def gradients(self) -> dict: """Gets all gradients @return: GET response dictionary """ return self.options def apply(self, prj_name:str): """Applies a gradient to a project @project: Name of project to apply gradient to @return: PATCH response dictionary """ project = Project(self.instance) prj_id = project.get(prj_name)["item"]["id"] if "type" not in self.data.keys(): raise InvalidToken("Please specify a background type: `gradient` | `image`") if self.data["type"] == "gradient" and self.data["name"] not in self.options: raise InvalidToken(f"Gradient {self.data['name']} not found: please choose from\n{self.options}") return super().update(f"/api/projects/{prj_id}", data={"background": self.data}) def clear(self, prj_name:str): """Clears a gradient from a project @project: Name of project to clear gradient from @return: PATCH response dictionary """ project = Project(self.instance) prj_id = project.get(prj_name)["item"]["id"] return super().update(f"/api/projects/{prj_id}", data={"background": None}) class Comment(Controller): def __init__(self, instance:Planka, **kwargs) -> None: self.instance = instance self.template = instance.get_template("comment-action") self.data = self.build(**kwargs) class User(Controller): def __init__(self, instance:Planka, **kwargs) -> None: """Creates a user @username: Username of user to create @name: Display name of user to create @password: Password of user to create @email: Email of user to create @subscribe: Subscibe user to own cards (default: False) @organization: Organization of user to create (default: None) @admin: Admin state of user to create (default: False) """ self.instance = instance self.template = instance.get_template("user") self.data = self.build(**kwargs) def get(self, username:str=None): """Gets a user @username: Username of user to get (all if not provided) @return: GET response dictionary """ if not username: return super().get("/api/users")["items"] users = super().get("/api/users")["items"] names = [user["username"] for user in users] if username not in names: raise InvalidToken(f"User {username} not found") return users[names.index(username)] def create(self, data:dict=None): """Creates a user @data: Data dictionary to create user with (optional) @return: POST response dictionary """ if not data: data = self.data if not data: raise InvalidToken("Please either build a user or provide a data dictionary") if self.data["username"] in [user["username"] for user in self.get()]: raise InvalidToken(f"User {self.data['username']} already exists") return super().create("/api/users", data=self.data) def delete(self, username:str, oid:str=None): """Deletes a user @username: Username of user to delete @oid: ID of user to delete (Use this if you already have the ID) @return: DELETE response dictionary """ if oid: return super().delete(f"/api/users/{oid}") if username not in [user["username"] for user in self.get()]: raise InvalidToken(f"User {username} not found") return super().delete(f"/api/users/{self.get(username)['id']}") def update(self, username:str, oid:str=None, data:dict=None): """Updates a user @username: Username of user to update @oid: ID of user to update (Use this if you already have the ID) @data: Data dictionary to update user with (optional) @return: PATCH response dictionary """ user = self.get(username) if not data: data = self.data if not data: raise InvalidToken("Please either build a user or provide a data dictionary") return super().update(f"/api/users/{user['id']}", data=data) class InvalidToken(Exception): """General Error for invalid API inputs """ pass