commit ae3e4668b7e5de651a6816d608728f760a6283f5 Author: hwelch-fle <91618355+hwelch-fle@users.noreply.github.com> Date: Thu Mar 23 21:24:46 2023 -0400 initial commit diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..1ae9c78 --- /dev/null +++ b/__init__.py @@ -0,0 +1,2 @@ +from plankapy import Planka +from controllers import Project, Board, Card, List, Stopwatch, Label, Task, CommentAction, Attachment, User, CardMembership, BoardMembership, CardLabel, ProjectManager, Background \ No newline at end of file diff --git a/config/planka_routes.json b/config/planka_routes.json new file mode 100644 index 0000000..3c3ba3d --- /dev/null +++ b/config/planka_routes.json @@ -0,0 +1,231 @@ +{"access-token": + { + "DELETE": + { + "active": "/api/access-tokens/me" + }, + "GET": null, + "PATCH": null, + "POST": + { + "active": "/api/access-tokens" + } + }, + "attachment": + { + "DELETE": + { + "attachment": "/api/attachments/:id:" + }, + "GET": null, + "PATCH": + { + "attachment": "/api/attachments/:id:" + }, + "POST": null + }, + "board-membership": + { + "DELETE": + { + "board-membership": "/api/board-memberships/:id:" + }, + "GET": null, + "PATCH": + { + "board-membership": "/api/board-memberships/:id:" + }, + "POST": null + }, + "board": + { + "DELETE": + { + "board": "/api/boards/:id:" + }, + "GET": + { + "board": "/api/boards/:id:" + }, + "PATCH": + { + "board": "/api/boards/:id:" + }, + "POST": + { + "board-membership": "/api/boards/:id:/memberships", + "label": "/api/boards/:id:/labels", + "list": "/api/boards/:id:/lists" + } + }, + "card-membership": + { + "DELETE": + { + "card-membership": "/api/cards/:id:/memberships" + }, + "GET": null, + "PATCH": null, + "POST": + { + "card-membership": "/api/cards/:id:/memberships" + } + }, + "card": + { + "DELETE": + { + "card": "/api/cards/:id:", + "membership": "/api/cards/:id:/memberships", + "label": "/api/cards/:id:/labels/:id:" + }, + "GET": + { + "card": "/api/cards/:id:", + "actions": "/api/cards/:id:/actions" + }, + "PATCH": + { + "card": "/api/cards/:id:" + }, + "POST": + { + "membership":"/api/cards/:id:/memberships", + "label":"/api/cards/:id:/labels", + "task":"/api/cards/:id:/tasks", + "attachment":"/api/cards/:id:/attachments", + "comment-action":"/api/cards/:id:/comment-actions" + } + }, + "comment-action": + { + "DELETE": + { + "comment-action": "/api/comment-actions/:id:" + }, + "GET": null, + "PATCH": + { + "comment-action": "/api/comment-actions/:id:" + }, + "POST": null + }, + "label": + { + "DELETE": + { + "label": "/api/labels/:id:", + "card-label": "/api/cards/:id:/labels/:id:" + }, + "GET": null, + "PATCH": + { + "label": "/api/labels/:id:" + }, + "POST": + { + "card-label":"/api/cards/:id:/labels", + "board-label": "/api/boards/:id:/labels" + } + }, + "list": + { + "DELETE": + { + "list": "/api/lists/:id:" + }, + "GET": null, + "PATCH": + { + "list": "/api/lists/:id:" + }, + "POST": + { + "card": "/api/lists/:id:/cards" + } + }, + "notification": + { + "DELETE": null, + "GET": + { + "notifications": "/api/notifications", + "notification": "/api/notifications/:id:" + }, + "PATCH": + { + "notification": "/api/notifications/:id:" + }, + "POST": null + }, + "project-manager": + { + "DELETE": + { + "project-manager": "/api/project-managers/:id:" + }, + "GET": null, + "PATCH": null, + "POST": null + }, + "project": + { + "DELETE": + { + "project": "/api/projects/:id:" + }, + "GET": + { + "projects": "/api/projects", + "project": "/api/projects/:id:" + }, + "PATCH": + { + "project": "/api/projects/:id:" + }, + "POST": + { + "project": "/api/projects", + "background-image": "/api/projects/:id:/background-image", + "project-manager": "/api/projects/:id:/managers", + "board": "/api/projects/:id:/boards" + } + }, + "task": + { + "DELETE": + { + "task": "/api/tasks/:id:" + }, + "GET": null, + "PATCH": + { + "task": "/api/tasks/:id:" + }, + "POST": null + }, + "user": + { + "DELETE": + { + "user": "/api/users/:id:" + }, + "GET": + { + "users": "/api/users", + "user": "/api/users/:id:" + }, + "PATCH": + { + "user": "/api/users/:id:", + "user-email": "/api/users/:id:/email", + "user-password": "/api/users/:id:/password", + "user-username": "/api/users/:id:/username" + }, + "POST": + { + "user": "/api/users", + "user-avatar": "/api/users/:id:/avatar" + } + } +} diff --git a/config/planka_templates.json b/config/planka_templates.json new file mode 100644 index 0000000..ef22225 --- /dev/null +++ b/config/planka_templates.json @@ -0,0 +1,143 @@ +{ + "project": + { + "name": "str" + }, + "board": + { + "name": "str", + "type": "str", + "position": "int" + }, + "list": + { + "name": "str", + "position": "int" + }, + "card": + { + "name": "str", + "description": "str", + "position": "int", + "dueDate": "str", + "stopwatch": "Stopwatch" + }, + "task": + { + "name": "str", + "isCompleted": "bool", + "position": "int" + }, + "label": + { + "name": "str", + "color": "str", + "position": "int" + }, + "card-label": + { + "cardId": "int", + "labelId": "int" + }, + "card-membership": + { + "cardId": "int", + "userId": "int" + }, + "board-membership": + { + "boardId": "int", + "userId": "int" + }, + "project-manager": + { + "projectId": "int", + "userId": "int" + }, + "user": + { + "name": "str", + "email": "str", + "password": "str", + "username": "str", + "phone": "str", + "organization": "str", + "subscribeToOwnCards": "bool" + }, + "comment-action": + { + "cardId": "str", + "text": "str" + }, + "attachment": + { + "cardId": "int", + "requestId": "int" + }, + "stopwatch": + { + "startedAt": "str", + "total": "int" + }, + "background": + { + "name": "str", + "type": "str" + }, + "gradients": + [ + "old-lime", + "ocean-dive", + "tzepesch-style", + "jungle-mesh", + "strawberry-dust", + "purple-rose", + "sun-scream", + "warm-rust", + "sky-change", + "green-eyes", + "blue-xchange", + "blood-orange", + "sour-peel", + "green-ninja", + "algae-green", + "coral-reef", + "steel-grey", + "heat-waves", + "velvet-lounge", + "purple-rain", + "blue-steel", + "blueish-curve", + "prism-light", + "green-mist", + "red-curtain" + ], + "colors": + [ + "berry-red", + "pumpkin-orange", + "lagoon-blue", + "pink-tulip", + "light-mud", + "orange-peel", + "bright-moss", + "antique-blue", + "dark-granite", + "lagune-blue", + "sunny-grass", + "morning-sky", + "light-orange", + "midnight-blue", + "tank-green", + "gun-metal", + "wet-moss", + "red-burgundy", + "light-concrete", + "apricot-red", + "desert-sand", + "navy-blue", + "egg-yellow", + "coral-green", + "light-cocoa" + ] +} \ No newline at end of file diff --git a/controllers.py b/controllers.py new file mode 100644 index 0000000..5542803 --- /dev/null +++ b/controllers.py @@ -0,0 +1,359 @@ +from plankapy import Planka +import time + +OFFSET = 65535 +REFRESH = 10 # Seconds + +class Controller(object): + def __init__(self, instance:Planka, **kwargs:dict): + self.instance:Planka = instance + self.kwargs:dict = kwargs + self.template:dict = None + self.routes:dict = None + self.active:dict = None + + def get_route(self, method:str, action:str) -> str: + """Gets the routes for the controller + @return: Routes + """ + if self.routes == None: + raise Exception("Routes not loaded") + if method not in self.routes: + raise Exception(f"Invalid method {method} for route {action}") + return self.routes[method][action] + + def parse_route(self, method:str=None, action:str=None ,id:str=None, parent:object=None, req_id:bool=False) -> str: + """Generates a route string from parameters + @method: HTTP method + @action: HTTP action + @id(optional): ID to use in route + @parent: Parent controller + @req_id: Whether or not the route requires an ID + (Parent ID will be used if needed) + @return: Route string + """ + route = self.get_route(method, action) + if route.count(":id:") > 1: + if parent == None: + raise Exception(f"Parent required for route '{route}'") + route = route.replace(":id:", str(parent.active()["id"]), 1) + if req_id: + route = route.replace(":id:", str(id), 1) + return route + + def get_active(self) -> dict: + """Gets the active object for the controller + @return: Active object + """ + if self.active: + return self.active + + def set_active(self, active:dict) -> dict: + """Sets the active object for the controller + @active: Active object + @return: Previous active object + """ + old = self.active + self.active = active + return old + + def clear_active(self) -> None: + """Clears the active object for the controller + @return: None + """ + self.active = None + return self.active + + def get_template(self) -> dict: + """Gets the template for the controller + @return: Template + """ + raise NotImplementedError("get_template() not implemented") + + def build(self) -> dict: + """Builds the object to be sent to the Planka API + @return: Object to be sent to Planka API + """ + keys = list(self.template.keys()) + for key in self.kwargs: + if key in self.template: + self.template[key] = self.kwargs[key] + keys.remove(key) + if len(keys) > 0: + for key in keys: + self.template[key] = None + return self.template + + def create(self, **kwargs) -> dict: + """Creates an object in Planka + @return: Object created in Planka + """ + self.active = self.instance.request("POST", self.parse_route(method="POST", **kwargs), data=self.build()) + return self.active + + def update(self, data:dict=None, **kwargs) -> dict: + """Patches an object in Planka + @data: Data to patch + @return: Object patched in Planka + """ + return self.instance.request("PATCH", self.parse_route(method="PATCH", **kwargs), data=data) + + def delete(self, **kwargs) -> None: + """Deletes an object in Planka + @return: None + """ + return self.instance.request("DELETE", self.parse_route(method="DELETE", **kwargs)) + + def get(self, **kwargs) -> dict: + """Gets an object by ID + @kwargs: Additional arguments passed to _parse_route() + : @id: ID of object + : @route_parent(str): The dictionary key for the route parent in planka_routes.json\n + : @route_key(str): The dictionary key for the route in planka_routes.json\n + : @parent(Controller): Parent controller object\n + : @req_id(bool): Whether or not the route requires an ID\n + @return: Object + """ + return self.instance.request("GET", self.parse_route(method="GET", **kwargs)) + +class Project(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["project"] + self.routes = self.instance.routes["project"] + self.project_dict = None + self.last_updated = None + self.active = {"name": None, "id": None, "content": None} + + def create(self) -> dict: + if self.kwargs['name'] in self.get_project_names(): + self.active=self.get_project_by_name(self.kwargs['name']) + print(Exception(f"Project '{self.kwargs['name']}' already exists, setting active project to '{self.kwargs['name']}'")) + return self.active + project_item = super().create(action="project")['item'] + self.active['name'] = self.kwargs['name'] + self.active['id'] = project_item['id'] + self.active['content'] = None + self.get_project_dictionary() + return self.active + + def delete(self, id:str=None) -> None: + if id == None: + id = self.active["id"] + deleted = super().delete(id=id, action="project", req_id=True) + self.get_project_dictionary() + return deleted + + def update(self, id:str=None, data:dict=None) -> dict: + if id == None: + id = self.active["id"] + return super().update(id=id, action="project",data=data, req_id=True) + + def set_active(self, name:str) -> dict: + active = self.get_project_by_name(name) + return super().set_active(active) + + def get_project(self, id:str=None) -> dict: + if id == None: + id = self.active()["id"] + return super().get(id=id, action="project", req_id=True)['included'] + + def get_projects(self) -> list: + return super().get(action="projects") + + def get_project_names(self): + if self.project_dict == None: + self.project_dict = self.get_project_dictionary() + return list(self.project_dict.keys()) + + def get_project_dictionary(self) -> dict: + if self.last_updated == None or time.time() - self.last_updated > REFRESH: + self.last_updated = time.time() + self.project_dict = self.get_project_dictionary() + self.project_dict = {project['name']: {"name": project['name'] ,"id": project['id'], "content":self.get_project(project['id'])} for project in self.get_projects()['items']} + return self.project_dict + + def get_project_by_name(self, name:str=None) -> dict: + return self.get_project_dictionary()[name] + + def get_project_boards(self, name:str=None) -> list: + if name == None: + name = self.active["name"] + return self.get_project_dictionary()[name]["content"]['boards'] + + def get_project_users(self, name:str=None) -> list: + if name == None: + name = self.active["name"] + return self.get_project_dictionary()[name]["content"]['users'] + + def get_project_board_memberships(self, name:str=None) -> list: + if name == None: + name = self.active["name"] + return self.get_project_dictionary()[name]["content"]['boardMemberships'] + + def get_project_managers(self, name:str=None) -> list: + if name == None: + name = self.active["name"] + return self.get_project_dictionary()[name]["content"]['projectManagers'] + + def get_project_board_names(self, name:str=None) -> list: + if name == None: + name = self.active["name"] + return [board['name'] for board in self.get_project_boards(name)] + +class Board(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["board"] + self.routes = self.instance.routes["board"] + self.board_dict = None + self.active = {"name": None, "id": None, "project": None, "content": None} + + def set_project(self, project:str) -> dict: + self.active['project'] = project + return self.active + + def set_active(self, active: dict) -> dict: + self.active["name"] = active["name"] + self.active["id"] = active["id"] + self.active["project"] = active["project"] + self.active["content"] = active["content"] + return self.active + + def get_board(self, name:str=None, projectName:str=None, projectController:Project=None) -> dict: + if name == None and self.active["name"] != None: + name = self.active["name"] + if name == None: + raise Exception("No board provided") + if projectController == None: + projectController = Project(instance=self.instance) + if projectName == None and projectController.active["name"] != None: + projectName = projectController.active["name"] + if projectName == None: + raise Exception("No project provided") + valid_names = projectController.get_project_board_names(projectName) + if name not in valid_names: + raise Exception(f"Board '{name}' not found in project '{projectName}'") + board_id = [board for board in projectController.get_project_boards(projectName) if board['name'] == name][0]['id'] + board = super().get(id=board_id, action="board", req_id=True)['item'] + return self.get(id=board_id, action="board", req_id=True) + + def get_boards(self, projectController:Project=None, name:str=None) -> list: + if name == None and projectController.active["name"] != None: + name = projectController.active["name"] + projectController.get_project_boards(name) + +class List(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["list"] + self.routes = self.instance.routes["list"] + self.active = {"name": None, "id": None, "board": None ,"content": None} + +class Stopwatch(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["stopwatch"] + self.active = {"name": None, "id": None, "project": None, "board": None , "card": None ,"content": None} + +class Card(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["card"] + self.routes = self.instance.routes["card"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None ,"content": None} + +class Label(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["label"] + self.routes = self.instance.routes["label"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None , "card": None ,"content": None} + +class Task(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["task"] + self.routes = self.instance.routes["task"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None , "card": None ,"content": None} + +class CommentAction(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["comment-action"] + self.routes = self.instance.routes["comment-action"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None , "card": None ,"content": None} + +class Attachment(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["attachment"] + self.routes = self.instance.routes["attachment"] + +class User(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["user"] + self.routes = self.instance.routes["user"] + self.active = {"name": None, "id": None, "content": None} + +class CardMembership(Controller): + def __init__(self,instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["card-membership"] + self.routes = self.instance.routes["card-membership"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None , "card": None ,"content": None} + +class BoardMembership(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["board-membership"] + self.routes = self.instance.routes["board-membership"] + self.active = {"name": None, "id": None, "project": None, "board": None , "content": None} + +class CardLabel(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["card-label"] + self.routes = self.instance.routes["label"] + self.active = {"name": None, "id": None, "project": None, "board": None , "list": None , "card": None ,"content": None} + +class ProjectManager(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance = instance + self.kwargs = kwargs + self.template = self.instance.templates["project-manager"] + self.routes = self.instance.routes["project-manager"] + self.active = {"name": None, "id": None, "user": None ,"project": None, "content": None} + +class Background(Controller): + def __init__(self, instance:Planka=None, **kwargs): + self.instance:Planka = instance + self.kwargs:dict = kwargs + self.template:dict = self.instance.templates["background"] + self.gradients:list = self.instance.templates["gradients"] + +planka = Planka("http://planka.corp.finelines-engineering.com", "hwelch", "Fiber4u!") + +projectController = Project(planka) + +projectController.set_active("Management API") + +boardController = Board(planka) + +boards = projectController.get_project_boards() + +boardController.get_board(name=boards[0]['name'], projectController=projectController) \ No newline at end of file diff --git a/plankapy.py b/plankapy.py new file mode 100644 index 0000000..2eb1076 --- /dev/null +++ b/plankapy.py @@ -0,0 +1,841 @@ +import requests +import json +import time + +API_URL = "http://127.0.0.1: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, api_end="/api", routes="config/planka_routes.json", templates="config/planka_templates.json"): + self.url = url + self.username = username + self.password = password + self.api_end = api_end + self.auth = None + with open(routes) as f: + self.routes = json.load(f) + with open(templates) as f: + self.templates = json.load(f) + self.authenticate() + + def deauthenticate(self) -> bool: + """Deletes the auth token from the Planka API + @return: True if successful, False if not + """ + try: + self.request("DELETE", self.get_route("access-token", "DELETE", "active")) + self.auth = None + return True + except: + return False + + def validate(self) -> bool: + """Validates the Planka API connection + @return: True if successful, False if not + """ + try: + self.request("GET", "/*") + return True + except: + return False + + 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}{self.get_route('access-token', 'POST', 'active')}", data={'emailOrUsername': self.username, 'password': self.password}) + self.auth = request.json()['item'] + if self.auth == None: + raise Exception("Invalid API credentials") + return True + except: + raise Exception("Invalid API credentials") + + 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 self.auth == None: + 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 Exception("Invalid API credentials") + + if response.status_code not in [200, 201]: + raise Exception(f"Failed to {method} {url} with status code {response.status_code}") + + try: + return response.json() + except: + raise Exception(f"Failed to parse response from {url}") + + def get_route(self, controller:str, method:str, action:str) -> str: + """Returns a route from the planka_routes.json file + @controller: Name of controller + @method: HTTP method + @action: Name of request action + @return: Route string + """ + try: + return self.routes[controller][method][action] + except: + raise Exception(f"Route not found: {controller} {method} {action}") + + 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 Exception(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 kwargs == {}: + return kwargs + data = {} + valid_keys = self.template.keys() + for key, value in kwargs.items(): + if key in valid_keys: + data[key] = value + 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 Exception(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 Exception(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(f"/api/projects") + if not name: + return prjs + prj_names = [prj["name"] for prj in prjs["items"]] + if name not in prj_names: + raise Exception(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 Exception(f"Please Build a {type(self).__name__} before creating") + if self.data["name"] in [prj["name"] for prj in self.get()['items']]: + raise Exception(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 Exception("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 Exception(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 self.data == None: + raise Exception(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 Exception(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 Exception("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 project_name == None: + raise Exception("Please provide a project name") + if board_name == None: + raise Exception("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 Exception("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 list_name == None: + return lists + if list_name not in list_names: + raise Exception(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 Exception(f"Please Build a {type(self).__name__} before creating") + if not (project_name and board_name): + raise Exception("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 Exception(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 Exception("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 != None: + return super().delete(f"/api/lists/{oid}") + if not (project_name and board_name and list_name): + raise Exception("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 Exception("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 card_name == None: + return [self.get(oid=card["id"]) for card in cards] + if card_name not in card_names: + raise Exception(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 data == None: + data = self.data + if data == None: + raise Exception(f"Please Build a {type(self).__name__} before creating") + if not (project_name and board_name and list_name): + raise Exception("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 Exception("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 Exception(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 Exception("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 Exception("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 Exception("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 Exception(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 Exception(f"Please Build a {type(self).__name__} before creating") + if not (project_name and board_name): + raise Exception("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 Exception("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 Exception("Please provide a project, board, label name") + if card_id and label_name: + 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 Exception("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 Exception("Please provide a project, board, label name") + if card_id and label_name: + 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 Exception("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 Exception("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 task_name == None: + return tasks + if task_name not in task_names: + raise Exception(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 Exception(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 Exception("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 Exception(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 Exception("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 Exception("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 Exception("Please specify a background type: `gradient` | `image`") + if self.data["type"] == "gradient" and self.data["name"] not in self.options: + raise Exception(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: + self.instance = instance + self.template = instance.get_template("user") + self.data = self.build(**kwargs) + +def test_planka(): + import random + planka = Planka(API_URL, API_USER, API_PASS) + project = Project(planka) + board = Board(planka) + lst = List(planka) + card = Card(planka) + label = Label(planka) + task = Task(planka) + attachment = Attachment(planka) + stopwatch = Stopwatch(planka) + background = Background(planka) + comment = Comment(planka) + user = User(planka) + + if "Plankapy Test Project" in [prj["name"] for prj in project.get()["items"]]: + project.delete("Plankapy Test Project") + + + project.build(name="Plankapy Test Project") + project.create() + print("Created Test Project") + + board.build(name="Test Board", type="kanban", position=OFFSET) + board.create("Plankapy Test Project") + print("Created Test Board") + + next_pos = OFFSET + new_labels = [] + for color in label.colors(): + label.build(name=f"{color} label", color=color, position=next_pos) + new_labels.append(label.create("Plankapy Test Project", "Test Board")["item"]) + next_pos += OFFSET + print(f"Created {color} Label") + lst.build(name="Test List", position=0) + lst.create("Plankapy Test Project", "Test Board") + print("Created Test List") + + new_cards=[] + next_pos = OFFSET + for i in range(1, 11): + card.build(name=f"Test Card {i}", description=f"CHANGE ME {i}", position=next_pos) + next_pos += OFFSET + new_cards.append(card.create("Plankapy Test Project", "Test Board", "Test List")["item"]) + print(f"Created Test Card {i}") + + for cd in new_cards: + lb = random.choice(new_labels) + label.add(label_id=lb["id"], card_id=cd["id"]) + print("added random labels to cards") + + for i in range(int(len(new_cards)/2)): + cd = random.choice(new_cards) + lbs = card.get_labels("Plankapy Test Project", "Test Board", oid=cd["id"]) + for lb in lbs: + print(lb) + label.remove(label_id=lb["labelId"], card_id=lb["cardId"]) + print(f"removed label from {cd['name']}") + print("removed random labels from half the cards") + + new_tasks=[] + for cd in new_cards: + next_pos=OFFSET + for i in range(1,5): + task.build(name=f"Test Task {i}", position=next_pos) + next_pos += OFFSET + print(cd) + new_tasks.append(task.create(card_id=cd["id"])["item"]) + print(f"Created 4 tasks on {cd['name']}") + + for tsk in new_tasks: + print(tsk) + task.build(name=f"Updated Task: {tsk['name']}", isCompleted=True) + task.update(oid=tsk["id"]) + print("Updated task all tasks") + + for grad in background.gradients(): + grad = grad + background.build(name=grad, type="gradient") + background.apply("Plankapy Test Project") + print(f"Applied gradient {grad} to Test Project") + + background.clear("Plankapy Test Project") + print("Cleared gradient from Test Project") + print("Tests complete") diff --git a/tests/plankapy_tests.py b/tests/plankapy_tests.py new file mode 100644 index 0000000..3024f09 --- /dev/null +++ b/tests/plankapy_tests.py @@ -0,0 +1,41 @@ +# -*- coding: utf-8 -*- +from plankapy import Planka +from controllers import Project, Board, Card, List, Stopwatch, Label, Task, CommentAction, Attachment, User, CardMembership, BoardMembership, CardLabel, ProjectManager, Background + +API_URL = "http://planka.corp.finelines-engineering.com" +API_USER = "hwelch" +API_PASS = "Fiber4u!" + +planka = Planka(API_URL, API_USER, API_PASS) + +ProjectController = Project(instance=planka, name="Test Project 2", description="Test Project Description") +BoardController = Board(instance=planka) +CardController = Card(instance=planka) +ListController = List(instance=planka) +StopwatchController = Stopwatch(instance=planka) +LabelController = Label(instance=planka) +TaskController = Task(instance=planka) +CommentActionController = CommentAction(instance=planka) +AttachmentController = Attachment(instance=planka) +UserController = User(instance=planka) +CardMembershipController = CardMembership(instance=planka) +BoardMembershipController = BoardMembership(instance=planka) +CardLabelController = CardLabel(instance=planka) +ProjectManagerController = ProjectManager(instance=planka) +BackgroundController = Background(instance=planka, name = "sky-change", type="gradient") + +print(f"Project:\n\t{ProjectController.build()}") +print(f"Board:\n\t{BoardController.build()}") +print(f"Card:\n\t{CardController.build()}") +print(f"List:\n\t{ListController.build()}") +print(f"Stopwatch:\n\t{StopwatchController.build()}") +print(f"Label:\n\t{LabelController.build()}") +print(f"Task:\n\t{TaskController.build()}") +print(f"CommentAction:\n\t{CommentActionController.build()}") +print(f"Attachment:\n\t{AttachmentController.build()}") +print(f"User:\n\t{UserController.build()}") +print(f"CardMembership:\n\t{CardMembershipController.build()}") +print(f"BoardMembership:\n\t{BoardMembershipController.build()}") +print(f"CardLabel:\n\t{CardLabelController.build()}") +print(f"ProjectManager:\n\t{ProjectManagerController.build()}") +print(f"Background:\n\t{BackgroundController.build()}") \ No newline at end of file