refactor: restructure project for pip packaging
Some checks failed
website / build (push) Failing after 30s

Restructured the entire project directory, making `plankapy` pip-installable, enhancing its modularity and maintainability. This involved moving and renaming Python source files and tests into a dedicated `src/plankapy` structure, introducing a `pyproject.toml` for modern packaging standards, and removing obsolete `setup.py` and `__init__.py` files. Updated CI/CD pipelines to accommodate the new structure, ensuring continued automation efficiency. Added a `test_config.py` for centralized test configuration, improving test reliability and ease of modification.

- Migrated project documentation and workflow under new `.forgejo` directory for consistency with Forgejo platform requirements.
- Enhanced Docker containerization in CI builds by specifying a node-based container, streamlining the environment setup process.
- Expanded `.gitignore` to include `venv/`, promoting local development best practices.
- Introduced a comprehensive README update, clarifying the project's forked nature and pip-installation goal, fostering better community understanding and contribution suitability.
This commit is contained in:
Kumi 2024-04-25 08:48:11 +02:00
parent 5c5d03c309
commit d5777fac54
Signed by: kumi
GPG key ID: ECBCC9082395383F
12 changed files with 448 additions and 195 deletions

View file

@ -16,13 +16,13 @@ permissions:
jobs: jobs:
# Build the documentation and upload the static HTML files as an artifact. # Build the documentation and upload the static HTML files as an artifact.
build: build:
runs-on: ubuntu-latest container:
image: node:20-bookworm
steps: steps:
- uses: actions/checkout@v3 - name: Install dependencies
- uses: actions/setup-python@v4 run: |
with: apt update
python-version: '3.11' apt install -y python3 python3-pip
# ADJUST THIS: install all dependencies (including pdoc) # ADJUST THIS: install all dependencies (including pdoc)
#- run: pip install -e . #- run: pip install -e .
#- run: pip install --upgrade pip #- run: pip install --upgrade pip
@ -31,23 +31,9 @@ jobs:
#- run: pip install json #- run: pip install json
# ADJUST THIS: build your documentation into docs/. # ADJUST THIS: build your documentation into docs/.
# We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here. # We use a custom build script for pdoc itself, ideally you just run `pdoc -o docs/ ...` here.
- run: python3 -m pdoc -d markdown -o ../../docs ../../plankapy.py - run: python3 -m pdoc -d markdown -o docs src/plankapy/plankapy.py
- uses: actions/upload-pages-artifact@v1 - uses: forgejo/upload-artifact@v4
with: with:
path: ../../docs path: docs
# Deploy the artifact to GitHub pages.
# This is a separate job so that only actions/deploy-pages has the necessary permissions.
deploy:
needs: build
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v2

1
.gitignore vendored
View file

@ -5,3 +5,4 @@
# Directories # Directories
__pycache__/ __pycache__/
venv/

View file

@ -1,6 +1,10 @@
# plankapy # plankapy
A python 3 based API for controlling a self-hosted Planka instance A python 3 based API for controlling a self-hosted Planka instance
This is a fork of the original [plankapy](https://github.com/hwelch-fle/plankapy)
project by [hwelch-fle](https://github.com/hwelch-fle) that primarily focuses on
making the project a pip installable package.
[Docs](https://hwelch-fle.github.io/plankapy/plankapy.html) [Docs](https://hwelch-fle.github.io/plankapy/plankapy.html)
# Rest API Source # Rest API Source

View file

@ -1 +0,0 @@
import plankapy

28
pyproject.toml Normal file
View file

@ -0,0 +1,28 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
[project]
name = "plankapy"
version = "0.1.0"
authors = [
{ name="hwelch-fle" },
{ name="Private.coffee Team", email="support@private.coffee" },
]
description = "A python 3 based API for controlling a self-hosted Planka instance"
readme = "README.md"
license = { file="LICENSE" }
requires-python = ">=3.10"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU Affero General Public License v3",
"Operating System :: OS Independent",
]
dependencies = [
"requests"
]
[project.urls]
"Homepage" = "https://git.private.coffee/PrivateCoffee/plankapy"
"Bug Tracker" = "https://git.private.coffee/PrivateCoffee/plankapy/issues"
"Source Code" = "https://git.private.coffee/PrivateCoffee/plankapy"

View file

@ -1 +0,0 @@
## TODO

1
src/plankapy/__init__.py Normal file
View file

@ -0,0 +1 @@
from .plankapy import *

View file

@ -1,10 +1,12 @@
import requests import requests
import json import json
import os
from pathlib import Path
from typing import Optional
# TODO: Split this into multiple files
API_URL = "http://localhost:3000"
API_USER = "demo- demo.demo"
API_PASS = "demo"
OFFSET = 65535
class Planka: class Planka:
"""API wrapper class for Planka """API wrapper class for Planka
@ -12,16 +14,28 @@ class Planka:
- username: Username of Planka user - username: Username of Planka user
- password: Password of Planka user - password: Password of Planka user
""" """
def __init__(self, url:str, username:str, password:str, templates="config/templates.json"):
def __init__(
self,
url: str,
username: str,
password: str,
templates: Optional[os.PathLike] = None,
):
self.url = url self.url = url
self.username = username self.username = username
self.password = password self.password = password
self.auth = None self.auth = None
if not templates:
templates = Path(__file__).parent / "config" / "templates.json"
with open(templates) as f: with open(templates) as f:
self.templates = json.load(f) self.templates = json.load(f)
self.authenticate() self.authenticate()
def __repr__(self): def __repr__(self):
# TODO: Should this really include the plain-text password?
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>" 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: def deauthenticate(self) -> bool:
@ -33,7 +47,9 @@ class Planka:
self.auth = None self.auth = None
return True return True
except: except:
raise InvalidToken(f"No active access token assigned to this instance\n{self.__repr__()}") raise InvalidToken(
f"No active access token assigned to this instance\n{self.__repr__()}"
)
def validate(self) -> bool: def validate(self) -> bool:
"""Validates the Planka API connection """Validates the Planka API connection
@ -50,15 +66,18 @@ class Planka:
- **return:** True if successful, False if not - **return:** True if successful, False if not
""" """
try: try:
request = requests.post(f"{self.url}/api/access-tokens", data={'emailOrUsername': self.username, 'password': self.password}) request = requests.post(
self.auth = request.json()['item'] f"{self.url}/api/access-tokens",
data={"emailOrUsername": self.username, "password": self.password},
)
self.auth = request.json()["item"]
if not self.auth: if not self.auth:
raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}") raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}")
return True return True
except: except:
raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}") raise InvalidToken(f"Invalid API credentials\n{self.__repr__()}")
def request(self, method:str, endpoint:str, data:dict=None) -> dict: def request(self, method: str, endpoint: str, data: Optional[dict] = None) -> dict:
"""Makes a request to the Planka API """Makes a request to the Planka API
- method: HTTP method - method: HTTP method
- endpoint: API endpoint - endpoint: API endpoint
@ -67,10 +86,9 @@ class Planka:
""" """
if not self.auth: if not self.auth:
self.authenticate() self.authenticate()
headers = \ headers = {
{
"Content-Type": "application/json", "Content-Type": "application/json",
"Authorization": f"Bearer {self.auth}" "Authorization": f"Bearer {self.auth}",
} }
url = f"{self.url}{endpoint}" url = f"{self.url}{endpoint}"
response = requests.request(method, url, headers=headers, json=data) response = requests.request(method, url, headers=headers, json=data)
@ -79,7 +97,9 @@ class Planka:
raise InvalidToken("Invalid API credentials") raise InvalidToken("Invalid API credentials")
if response.status_code not in [200, 201]: if response.status_code not in [200, 201]:
raise InvalidToken(f"Failed to {method} {url} with status code {response.status_code}") raise InvalidToken(
f"Failed to {method} {url} with status code {response.status_code}"
)
try: try:
return response.json() return response.json()
@ -96,7 +116,8 @@ class Planka:
except: except:
raise InvalidToken(f"Template not found: {template}") raise InvalidToken(f"Template not found: {template}")
class Controller():
class Controller:
def __init__(self, instance: Planka) -> None: def __init__(self, instance: Planka) -> None:
"""Controller class for Planka API """Controller class for Planka API
- instance: Planka API instance - instance: Planka API instance
@ -110,7 +131,9 @@ class Controller():
"""Returns a string representation of the controller object """Returns a string representation of the controller object
- **return:** String representation of controller object - **return:** String representation of controller object
""" """
return f"{type(self).__name__}:\n{json.dumps(self.data, sort_keys=True, indent=4)}" return (
f"{type(self).__name__}:\n{json.dumps(self.data, sort_keys=True, indent=4)}"
)
def __repr__(self) -> str: def __repr__(self) -> str:
"""Returns a string representation of the controller object """Returns a string representation of the controller object
@ -148,7 +171,7 @@ class Controller():
""" """
return self.instance.request("GET", route) return self.instance.request("GET", route)
def update(self, route:str, data:dict=None) -> dict: def update(self, route: str, data: Optional[dict] = None) -> dict:
"""Updates a controller object (PATCH) """Updates a controller object (PATCH)
- route: Route for controller object PATCH request - route: Route for controller object PATCH request
- oid: ID of controller object - oid: ID of controller object
@ -175,13 +198,14 @@ class Controller():
""" """
return self.response return self.response
class Project(Controller): class Project(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("project") self.template = instance.get_template("project")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
def get(self, name:str=None, oid:str=None) -> dict: def get(self, name: Optional[str] = None, oid: Optional[str] = None) -> dict:
"""Gets a project by name """Gets a project by name
- oid: ID of project to get (optional) - oid: ID of project to get (optional)
- name: Name of project if None returns all projects - name: Name of project if None returns all projects
@ -202,7 +226,7 @@ class Project(Controller):
"""Gets a list of project names """Gets a list of project names
- **return:** List of project names - **return:** List of project names
""" """
return [prj["name"] for prj in self.get()['items']] return [prj["name"] for prj in self.get()["items"]]
def create(self) -> dict: def create(self) -> dict:
"""Creates a new project """Creates a new project
@ -210,7 +234,7 @@ class Project(Controller):
""" """
if not self.data: if not self.data:
raise InvalidToken(f"Please Build a {type(self).__name__} before creating") raise InvalidToken(f"Please Build a {type(self).__name__} before creating")
if self.data["name"] in [prj["name"] for prj in self.get()['items']]: if self.data["name"] in [prj["name"] for prj in self.get()["items"]]:
raise InvalidToken(f"Project {self.data['name']} already exists") raise InvalidToken(f"Project {self.data['name']} already exists")
return super().create("/api/projects") return super().create("/api/projects")
@ -219,7 +243,7 @@ class Project(Controller):
- name: Name of project to update - name: Name of project to update
- **return:** PATCH response dictionary - **return:** PATCH response dictionary
""" """
prj_id = prj_id = self.get(name)['item']['id'] prj_id = prj_id = self.get(name)["item"]["id"]
return super().update(f"/api/projects/{prj_id}") return super().update(f"/api/projects/{prj_id}")
def delete(self, name: str) -> dict: def delete(self, name: str) -> dict:
@ -227,16 +251,22 @@ class Project(Controller):
- name: Name of project to delete - name: Name of project to delete
- **return:** DELETE response dictionary - **return:** DELETE response dictionary
""" """
prj_id = self.get(name)['item']['id'] prj_id = self.get(name)["item"]["id"]
return super().delete(f"/api/projects/{prj_id}") return super().delete(f"/api/projects/{prj_id}")
class Board(Controller): class Board(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("board") self.template = instance.get_template("board")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
def get(self, project_name:str=None, board_name:str=None, oid:str=None) -> dict: def get(
self,
project_name: Optional[str] = None,
board_name: Optionoal[str] = None,
oid: Optional[str] = None,
) -> dict:
"""Gets a board by name """Gets a board by name
- oid: ID of board to get (optonal) - oid: ID of board to get (optonal)
- name: Name of board if None returns all boards - name: Name of board if None returns all boards
@ -266,10 +296,16 @@ class Board(Controller):
if not self.data: if not self.data:
raise InvalidToken(f"Please Build a {type(self).__name__} before creating") raise InvalidToken(f"Please Build a {type(self).__name__} before creating")
prj_con = Project(self.instance) prj_con = Project(self.instance)
prj_id = prj_con.get(project_name)['item']['id'] prj_id = prj_con.get(project_name)["item"]["id"]
return super().create(f"/api/projects/{prj_id}/boards") 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: def update(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
data: Optional[dict] = None,
oid: Optional[str] = None,
) -> dict:
"""Updates a board """Updates a board
- oid: ID of board to update (optional) - oid: ID of board to update (optional)
- project_name: Name of project to update board in - project_name: Name of project to update board in
@ -284,10 +320,15 @@ class Board(Controller):
return super().update(f"/api/boards/{oid}", data=data) return super().update(f"/api/boards/{oid}", data=data)
if not (project_name and board_name): if not (project_name and board_name):
raise InvalidToken("Please provide project and board names") raise InvalidToken("Please provide project and board names")
board_id = self.get(project_name, board_name)['item']['id'] board_id = self.get(project_name, board_name)["item"]["id"]
return super().update(f"/api/boards/{board_id}", data=self.data) 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): def delete(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
oid: Optional[str] = None,
):
"""Deletes a board """Deletes a board
- oid: ID of board to delete (optional) - oid: ID of board to delete (optional)
- project_name: Name of project to delete board in - project_name: Name of project to delete board in
@ -300,16 +341,22 @@ class Board(Controller):
raise InvalidToken("Please provide a project name") raise InvalidToken("Please provide a project name")
if not board_name: if not board_name:
raise InvalidToken("Please provide a board name") raise InvalidToken("Please provide a board name")
board_id = self.get(project_name, board_name)['item']['id'] board_id = self.get(project_name, board_name)["item"]["id"]
return super().delete(f"/api/boards/{board_id}") return super().delete(f"/api/boards/{board_id}")
class List(Controller): class List(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("list") self.template = instance.get_template("list")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
def get(self, project_name:str=None, board_name:str=None, list_name:str=None): def get(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
list_name: Optional[str] = None,
):
"""Gets a list by name """Gets a list by name
NOTE: No GET route for list by ID NOTE: No GET route for list by ID
- project_name: Name of project to get list from - project_name: Name of project to get list from
@ -329,7 +376,12 @@ class List(Controller):
raise InvalidToken(f"List `{list_name}` not found") raise InvalidToken(f"List `{list_name}` not found")
return [lst for lst in lists if lst["name"] == list_name][0] 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): def create(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
data: Optional[dict] = None,
):
"""Creates a new list """Creates a new list
- project_name: Name of project to create list in - project_name: Name of project to create list in
- board_name: Name of board to create list in - board_name: Name of board to create list in
@ -342,10 +394,17 @@ class List(Controller):
if not (project_name and board_name): if not (project_name and board_name):
raise InvalidToken("Please provide project and board name") raise InvalidToken("Please provide project and board name")
board_con = Board(self.instance) board_con = Board(self.instance)
board_id = board_con.get(project_name, board_name)['item']['id'] board_id = board_con.get(project_name, board_name)["item"]["id"]
return super().create(f"/api/boards/{board_id}/lists") 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): def update(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
list_name: Optional[str] = None,
data: Optional[dict] = None,
oid: Optional[str] = None,
):
"""Updates a list """Updates a list
- oid: ID of list to update (optional) - oid: ID of list to update (optional)
- project_name: Name of project to update list in - project_name: Name of project to update list in
@ -364,7 +423,13 @@ class List(Controller):
lst = self.get(project_name, board_name, list_name) lst = self.get(project_name, board_name, list_name)
return super().update(f"/api/lists/{lst['id']}", data=data) 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): def delete(
self,
project_name: Optional[str] = None,
board_name: Optional[str] = None,
list_name: Optional[str] = None,
oid: Optional[str] = None,
):
"""Deletes a list """Deletes a list
- oid: ID of list to delete (optional) - oid: ID of list to delete (optional)
- project_name: Name of project to delete list in - project_name: Name of project to delete list in
@ -379,13 +444,21 @@ class List(Controller):
lst = self.get(project_name, board_name, list_name) lst = self.get(project_name, board_name, list_name)
return super().delete(f"/api/lists/{lst['id']}") return super().delete(f"/api/lists/{lst['id']}")
class Card(Controller): class Card(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("card") self.template = instance.get_template("card")
self.data = self.build(**kwargs) 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): 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 """Gets a card by name
- oid: ID of card to get (optional) - oid: ID of card to get (optional)
- project_name: Name of project to get card from - project_name: Name of project to get card from
@ -400,17 +473,27 @@ class Card(Controller):
raise InvalidToken("Please provide project, board, and list names") raise InvalidToken("Please provide project, board, and list names")
board_con = Board(self.instance) board_con = Board(self.instance)
board = board_con.get(project_name, board_name) board = board_con.get(project_name, board_name)
lst_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] lst_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][
cards = [card for card in board["included"]["cards"] if card["listId"] == lst_id] 0
]["id"]
cards = [
card for card in board["included"]["cards"] if card["listId"] == lst_id
]
card_names = [card["name"] for card in cards] card_names = [card["name"] for card in cards]
if not card_name: if not card_name:
return [self.get(oid=card["id"]) for card in cards] return [self.get(oid=card["id"]) for card in cards]
if card_name not in card_names: if card_name not in card_names:
raise InvalidToken(f"Card `{card_name}` not found") raise InvalidToken(f"Card `{card_name}` not found")
card_id = [card for card in cards if card["name"] == card_name][0]['id'] card_id = [card for card in cards if card["name"] == card_name][0]["id"]
return super().get(f"/api/cards/{card_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): def create(
self,
project_name: str = None,
board_name: str = None,
list_name: str = None,
data: dict = None,
):
"""Creates a new card """Creates a new card
- project_name: Name of project to create card in - project_name: Name of project to create card in
- board_name: Name of board to create card in - board_name: Name of board to create card in
@ -425,10 +508,19 @@ class Card(Controller):
raise InvalidToken("Please provide a project, board and list names") raise InvalidToken("Please provide a project, board and list names")
board_con = Board(self.instance) board_con = Board(self.instance)
board = board_con.get(project_name, board_name) board = board_con.get(project_name, board_name)
lst_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] 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") 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): 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 """Deletes a card
- oid: ID of card to delete (optional) - oid: ID of card to delete (optional)
- project_name: Name of project to delete card in - project_name: Name of project to delete card in
@ -444,7 +536,15 @@ class Card(Controller):
card = self.get(project_name, board_name, list_name, card_name) card = self.get(project_name, board_name, list_name, card_name)
return super().delete(f"/api/cards/{card['id']}") 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): 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 """Updates a card
- oid: ID of card to update (optional) - oid: ID of card to update (optional)
- project_name: Name of project to update card in - project_name: Name of project to update card in
@ -464,7 +564,14 @@ class Card(Controller):
card = self.get(project_name, board_name, list_name, card_name) card = self.get(project_name, board_name, list_name, card_name)
return super().update(f"/api/cards/{card['id']}", data=data) 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): 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 """Gets labels for a card
- oid: ID of card to get labels from (optional) - oid: ID of card to get labels from (optional)
- project_name: Name of project to get card from - project_name: Name of project to get card from
@ -474,11 +581,12 @@ class Card(Controller):
- **return:** GET response dictionary - **return:** GET response dictionary
""" """
if oid: if oid:
return self.get(oid=oid)['included']['cardLabels'] return self.get(oid=oid)["included"]["cardLabels"]
if not (project_name and board_name and list_name and card_name): if not (project_name and board_name and list_name and card_name):
raise InvalidToken("Please provide project, board, list, and card names") raise InvalidToken("Please provide project, board, list, and card names")
card_id = self.get(project_name, board_name, list_name, card_name)['item']['id'] card_id = self.get(project_name, board_name, list_name, card_name)["item"]["id"]
return self.get(oid=card_id)['included']['cardLabels'] return self.get(oid=card_id)["included"]["cardLabels"]
class Label(Controller): class Label(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
@ -490,7 +598,9 @@ class Label(Controller):
def colors(self) -> list: def colors(self) -> list:
return self.options return self.options
def get(self, project_name:str=None, board_name:str=None, label_name:str=None) -> dict: def get(
self, project_name: str = None, board_name: str = None, label_name: str = None
) -> dict:
"""Gets a label by name """Gets a label by name
- project_name: Name of project to get label from - project_name: Name of project to get label from
- board_name: Name of board to get label from - board_name: Name of board to get label from
@ -509,7 +619,9 @@ class Label(Controller):
raise InvalidToken(f"Label `{label_name}` not found") raise InvalidToken(f"Label `{label_name}` not found")
return [label for label in labels if label["name"] == label_name][0] 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): def create(
self, project_name: str = None, board_name: str = None, data: dict = None
):
"""Creates a new label """Creates a new label
- project_name: Name of project to create label in - project_name: Name of project to create label in
- board_name: Name of board to create label in - board_name: Name of board to create label in
@ -522,10 +634,16 @@ class Label(Controller):
if not (project_name and board_name): if not (project_name and board_name):
raise InvalidToken("Please provide project and board names") raise InvalidToken("Please provide project and board names")
board_con = Board(self.instance) board_con = Board(self.instance)
board = board_con.get(project_name, board_name)['item'] board = board_con.get(project_name, board_name)["item"]
return super().create(f"/api/boards/{board['id']}/labels") 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): def delete(
self,
project_name: str = None,
board_name: str = None,
label_name: str = None,
oid: str = None,
):
"""Deletes a label """Deletes a label
- oid: ID of label to delete (optional) - oid: ID of label to delete (optional)
- project_name: Name of project to delete label from - project_name: Name of project to delete label from
@ -540,7 +658,16 @@ class Label(Controller):
label = self.get(project_name, board_name, label_name) label = self.get(project_name, board_name, label_name)
return super().delete(f"/api/labels/{label['id']}") 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): 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 """Adds a label to a card
- project_name: Name of project to add label to card in - project_name: Name of project to add label to card in
- board_name: Name of board to add label to card in - board_name: Name of board to add label to card in
@ -550,20 +677,35 @@ class Label(Controller):
- **return:** POST response dictionary - **return:** POST response dictionary
""" """
if label_id and card_id: if label_id and card_id:
return super().create(f"/api/cards/{card_id}/labels", data={"labelId":label_id}) return super().create(
f"/api/cards/{card_id}/labels", data={"labelId": label_id}
)
if not (project_name and board_name and label_name): if not (project_name and board_name and label_name):
raise InvalidToken("Please provide a project, board, label name") raise InvalidToken("Please provide a project, board, label name")
if card_id: if card_id:
label = self.get(project_name, board_name, 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']}) return super().create(
f"/api/cards/{card_id}/labels", data={"labelId": label["item"]["id"]}
)
if not (card_name and list_name): if not (card_name and list_name):
raise InvalidToken("Please provide a card and list name") raise InvalidToken("Please provide a card and list name")
card_con = Card(self.instance) card_con = Card(self.instance)
card = card_con.get(project_name, board_name, list_name, card_name) card = card_con.get(project_name, board_name, list_name, card_name)
label = self.get(project_name, board_name, label_name) label = self.get(project_name, board_name, label_name)
return super().create(f"/api/cards/{card['item']['id']}/labels", {"labelId":label['item']['id']}) 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): 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 """Removes a label from a card
- project_name: Name of project to remove label from card in - project_name: Name of project to remove label from card in
- board_name: Name of board to remove label from card in - board_name: Name of board to remove label from card in
@ -577,14 +719,21 @@ class Label(Controller):
if not (project_name and board_name and label_name): if not (project_name and board_name and label_name):
raise InvalidToken("Please provide a project, board, label name") raise InvalidToken("Please provide a project, board, label name")
if card_id: if card_id:
label_id = [label['id'] for label in Card(self.instance).get_labels(oid=card_id) if label['name'] == label_name][0] 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_id}") return super().delete(f"/api/cards/{card_id}/labels/{label_id}")
if not (card_name and list_name): if not (card_name and list_name):
raise InvalidToken("Please provide a card and list name") raise InvalidToken("Please provide a card and list name")
card_con = Card(self.instance) card_con = Card(self.instance)
card = card_con.get(project_name, board_name, list_name, card_name) card = card_con.get(project_name, board_name, list_name, card_name)
label = self.get(project_name, board_name, label_name) label = self.get(project_name, board_name, label_name)
return super().delete(f"/api/cards/{card['item']['id']}/labels/{label['item']['id']}") return super().delete(
f"/api/cards/{card['item']['id']}/labels/{label['item']['id']}"
)
class Task(Controller): class Task(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
@ -592,7 +741,14 @@ class Task(Controller):
self.template = instance.get_template("task") self.template = instance.get_template("task")
self.data = self.build(**kwargs) 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: 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 """Gets a task by name
NOTE: No GET route for tasks by OID NOTE: No GET route for tasks by OID
- project_name: Name of project to get task from - project_name: Name of project to get task from
@ -606,10 +762,18 @@ class Task(Controller):
raise InvalidToken("Please provide project, board, list, and card names") raise InvalidToken("Please provide project, board, list, and card names")
board_con = Board(self.instance) board_con = Board(self.instance)
board = board_con.get(project_name, board_name) board = board_con.get(project_name, board_name)
list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][
cards = [card for card in board["included"]["cards"] if card["name"] == card_name and card["listId"] == list_id] 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"] 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] tasks = [
task for task in board["included"]["tasks"] if task["cardId"] == card_id
]
task_names = [task["name"] for task in tasks] task_names = [task["name"] for task in tasks]
if not task_name: if not task_name:
return tasks return tasks
@ -617,7 +781,15 @@ class Task(Controller):
raise InvalidToken(f"Task `{task_name}` not found") raise InvalidToken(f"Task `{task_name}` not found")
return [task for task in tasks if task["name"] == task_name][0] 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): 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 """Creates a new task
- card_id: ID of card to create task in (optional) - card_id: ID of card to create task in (optional)
- project_name: Name of project to create task in - project_name: Name of project to create task in
@ -636,12 +808,27 @@ class Task(Controller):
raise InvalidToken("Please provide project, board, list, and card names") raise InvalidToken("Please provide project, board, list, and card names")
board_con = Board(self.instance) board_con = Board(self.instance)
board = board_con.get(project_name, board_name) board = board_con.get(project_name, board_name)
list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][0]["id"] list_id = [ls for ls in board["included"]["lists"] if ls["name"] == list_name][
cards = [card for card in board["included"]["cards"] if card["name"] == card_name and card["listId"] == list_id] 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"] card_id = [card for card in cards if card["name"] == card_name][0]["id"]
return super().create(f"/api/cards/{card_id}/tasks") 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): 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 """Updates a task
- oid: Object ID of task to update (optional) - oid: Object ID of task to update (optional)
- project_name: Name of project to update task in - project_name: Name of project to update task in
@ -658,11 +845,21 @@ class Task(Controller):
if oid: if oid:
return super().update(f"/api/tasks/{oid}") return super().update(f"/api/tasks/{oid}")
if not (project_name and board_name and list_name and card_name and task_name): 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") raise InvalidToken(
"Please provide project, board, list, card, and task names"
)
task = self.get(project_name, board_name, list_name, card_name, task_name) task = self.get(project_name, board_name, list_name, card_name, task_name)
return super().update(f"/api/tasks/{task['id']}") 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): 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 """Deletes a task
- oid: ID of task to delete (Use this if you already have the ID) - oid: ID of task to delete (Use this if you already have the ID)
- project_name: Name of project to delete task from - project_name: Name of project to delete task from
@ -675,22 +872,27 @@ class Task(Controller):
if oid: if oid:
return super().delete(f"/api/tasks/{id}") return super().delete(f"/api/tasks/{id}")
if not (project_name and board_name and list_name and card_name and task_name): 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") raise InvalidToken(
"Please provide project, board, list, card, and task names"
)
task = self.get(project_name, board_name, list_name, card_name, task_name) task = self.get(project_name, board_name, list_name, card_name, task_name)
return super().delete(f"/api/tasks/{task['id']}") return super().delete(f"/api/tasks/{task['id']}")
class Attachment(Controller): class Attachment(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("attachment") self.template = instance.get_template("attachment")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
class Stopwatch(Controller): class Stopwatch(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("stopwatch") self.template = instance.get_template("stopwatch")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
class Background(Controller): class Background(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
@ -714,7 +916,9 @@ class Background(Controller):
if "type" not in self.data.keys(): if "type" not in self.data.keys():
raise InvalidToken("Please specify a background type: `gradient` | `image`") raise InvalidToken("Please specify a background type: `gradient` | `image`")
if self.data["type"] == "gradient" and self.data["name"] not in self.options: 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}") 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}) return super().update(f"/api/projects/{prj_id}", data={"background": self.data})
def clear(self, prj_name: str): def clear(self, prj_name: str):
@ -726,12 +930,14 @@ class Background(Controller):
prj_id = project.get(prj_name)["item"]["id"] prj_id = project.get(prj_name)["item"]["id"]
return super().update(f"/api/projects/{prj_id}", data={"background": None}) return super().update(f"/api/projects/{prj_id}", data={"background": None})
class Comment(Controller): class Comment(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
self.instance = instance self.instance = instance
self.template = instance.get_template("comment-action") self.template = instance.get_template("comment-action")
self.data = self.build(**kwargs) self.data = self.build(**kwargs)
class User(Controller): class User(Controller):
def __init__(self, instance: Planka, **kwargs) -> None: def __init__(self, instance: Planka, **kwargs) -> None:
"""Creates a user """Creates a user
@ -768,7 +974,9 @@ class User(Controller):
if not data: if not data:
data = self.data data = self.data
if not data: if not data:
raise InvalidToken("Please either build a user or provide a data dictionary") raise InvalidToken(
"Please either build a user or provide a data dictionary"
)
if self.data["username"] in [user["username"] for user in self.get()]: if self.data["username"] in [user["username"] for user in self.get()]:
raise InvalidToken(f"User {self.data['username']} already exists") raise InvalidToken(f"User {self.data['username']} already exists")
return super().create("/api/users", data=self.data) return super().create("/api/users", data=self.data)
@ -796,10 +1004,13 @@ class User(Controller):
if not data: if not data:
data = self.data data = self.data
if not data: if not data:
raise InvalidToken("Please either build a user or provide a data dictionary") raise InvalidToken(
"Please either build a user or provide a data dictionary"
)
return super().update(f"/api/users/{user['id']}", data=data) return super().update(f"/api/users/{user['id']}", data=data)
class InvalidToken(Exception): class InvalidToken(Exception):
"""General Error for invalid API inputs """General Error for invalid API inputs"""
"""
pass pass

View file

@ -0,0 +1,4 @@
API_URL = "http://localhost:3000"
API_USER = "demo- demo.demo"
API_PASS = "demo"
OFFSET = 65535

View file

@ -7,15 +7,14 @@ API_URL = None
API_USER = None API_USER = None
API_PASS = None API_PASS = None
default_tasks = \ default_tasks = [
[
"LLD", "LLD",
"LLD Invoiced", "LLD Invoiced",
"CD", "CD",
"CD Invoiced", "CD Invoiced",
"PD", "PD",
"PD Invoiced", "PD Invoiced",
"Constructed" "Constructed",
] ]
prj = input("Project: ") prj = input("Project: ")
@ -27,7 +26,9 @@ phase = input("Phase: ")
fdas = input("FDAs (comma seperated or - for range): ") fdas = input("FDAs (comma seperated or - for range): ")
stage = input("Stage (HLD | LLD | PD | CD) enter to match board: ") stage = input("Stage (HLD | LLD | PD | CD) enter to match board: ")
labels = input("Labels (comma seperated) enter to match board: ") labels = input("Labels (comma seperated) enter to match board: ")
print("Default Tasks: HLD, HLD Invoiced, LLD, LLD Invoiced, CD, CD Invoiced, PD, PD Invoiced, Constructed") print(
"Default Tasks: HLD, HLD Invoiced, LLD, LLD Invoiced, CD, CD Invoiced, PD, PD Invoiced, Constructed"
)
tasks = input("Tasks (comma seperated) enter for default: ") tasks = input("Tasks (comma seperated) enter for default: ")
print(f"cards will be created in\n\t{prj} \n\t |-> {brd} \n\t |-> {lst}") print(f"cards will be created in\n\t{prj} \n\t |-> {brd} \n\t |-> {lst}")
@ -55,5 +56,15 @@ desc = f"|Billable Footage | Stage | City |\n| -------- | -------- | -------- |\
instance = Planka(API_URL, API_USER, API_PASS) instance = Planka(API_URL, API_USER, API_PASS)
next_pos = OFFSET next_pos = OFFSET
for fda in fdas: for fda in fdas:
build_card(instance, project=prj, board=brd, list=lst, name=f"{mkt} {phase}.{fda}", description=desc, tasks=tasks, labels=labels, position=next_pos) build_card(
instance,
project=prj,
board=brd,
list=lst,
name=f"{mkt} {phase}.{fda}",
description=desc,
tasks=tasks,
labels=labels,
position=next_pos,
)
next_pos += OFFSET next_pos += OFFSET

View file

@ -1,6 +1,9 @@
from plankapy import * from plankapy import *
from plankapy.test_config import *
import random import random
def test_planka(): def test_planka():
## In no way meant to be efficient code, just a way to test all components ## In no way meant to be efficient code, just a way to test all components
## of the API ## of the API
@ -20,7 +23,6 @@ def test_planka():
if "Plankapy Test Project" in [prj["name"] for prj in project.get()["items"]]: if "Plankapy Test Project" in [prj["name"] for prj in project.get()["items"]]:
project.delete("Plankapy Test Project") project.delete("Plankapy Test Project")
project.build(name="Plankapy Test Project") project.build(name="Plankapy Test Project")
project.create() project.create()
print("Created Test Project") print("Created Test Project")
@ -34,41 +36,47 @@ def test_planka():
new_labels = {} new_labels = {}
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
new_labels[b['name']] = [] new_labels[b["name"]] = []
next_pos = OFFSET next_pos = OFFSET
for color in label.colors(): for color in label.colors():
label.build(name=f"{color} label", color=color, position=next_pos) label.build(name=f"{color} label", color=color, position=next_pos)
new_labels[b['name']].append(label.create("Plankapy Test Project", b["name"])["item"]) new_labels[b["name"]].append(
label.create("Plankapy Test Project", b["name"])["item"]
)
next_pos += OFFSET next_pos += OFFSET
print(f"Created {color} Label for Board {b['name']}") print(f"Created {color} Label for Board {b['name']}")
new_lists = {} new_lists = {}
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
new_lists[b['name']] = [] new_lists[b["name"]] = []
next_pos = OFFSET next_pos = OFFSET
for i in range(1, 5): for i in range(1, 5):
lst.build(name=f"Test List {i}", position=next_pos) lst.build(name=f"Test List {i}", position=next_pos)
new_lists[b['name']].append(lst.create("Plankapy Test Project", b["name"])) new_lists[b["name"]].append(lst.create("Plankapy Test Project", b["name"]))
next_pos += OFFSET next_pos += OFFSET
print(f"Created Test List {i} for Board {b['name']}") print(f"Created Test List {i} for Board {b['name']}")
new_cards = {} new_cards = {}
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
new_cards[b['name']] = [] new_cards[b["name"]] = []
next_pos = OFFSET next_pos = OFFSET
for i in range(1, 11): for i in range(1, 11):
card.build(name=f"Test Card {i}", description=f"CHANGE ME {i}", position=next_pos) card.build(
name=f"Test Card {i}", description=f"CHANGE ME {i}", position=next_pos
)
next_pos += OFFSET next_pos += OFFSET
new_cards[b['name']].append(card.create("Plankapy Test Project", b['name'], "Test List 1")["item"]) new_cards[b["name"]].append(
card.create("Plankapy Test Project", b["name"], "Test List 1")["item"]
)
print(f"Created Test Card {i} for Board {b['name']} in Test List 1") print(f"Created Test Card {i} for Board {b['name']} in Test List 1")
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
for cd in new_cards[b['name']]: for cd in new_cards[b["name"]]:
lb = random.choice(new_labels[b['name']]) lb = random.choice(new_labels[b["name"]])
label.add(label_id=lb["id"], card_id=cd["id"]) label.add(label_id=lb["id"], card_id=cd["id"])
print(f"added random labels to cards in board {b['name']}") print(f"added random labels to cards in board {b['name']}")
for _ in range(len(new_cards[b['name']]) // 2): for _ in range(len(new_cards[b["name"]]) // 2):
cd = random.choice(new_cards[b['name']]) cd = random.choice(new_cards[b["name"]])
lbs = card.get_labels("Plankapy Test Project", b["name"], oid=cd["id"]) lbs = card.get_labels("Plankapy Test Project", b["name"], oid=cd["id"])
for lb in lbs: for lb in lbs:
label.remove(label_id=lb["labelId"], card_id=lb["cardId"]) label.remove(label_id=lb["labelId"], card_id=lb["cardId"])
@ -77,16 +85,16 @@ def test_planka():
new_tasks = {} new_tasks = {}
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
new_tasks[b['name']] = [] new_tasks[b["name"]] = []
for cd in new_cards[b['name']]: for cd in new_cards[b["name"]]:
next_pos = OFFSET next_pos = OFFSET
for i in range(1, 5): for i in range(1, 5):
task.build(name=f"Test Task {i}", position=next_pos) task.build(name=f"Test Task {i}", position=next_pos)
next_pos += OFFSET next_pos += OFFSET
new_tasks[b['name']].append(task.create(card_id=cd["id"])["item"]) new_tasks[b["name"]].append(task.create(card_id=cd["id"])["item"])
print(f"Created 4 tasks on {cd['name']}") print(f"Created 4 tasks on {cd['name']}")
for b in board.get("Plankapy Test Project"): for b in board.get("Plankapy Test Project"):
for tsk in new_tasks[b['name']]: for tsk in new_tasks[b["name"]]:
task.build(name=f"Updated Task: {tsk['name']}", isCompleted=True) task.build(name=f"Updated Task: {tsk['name']}", isCompleted=True)
task.update(oid=tsk["id"]) task.update(oid=tsk["id"])
print("Updated task all tasks") print("Updated task all tasks")
@ -103,5 +111,6 @@ def test_planka():
print(f"Username: {us['username']}\nEmail: {us['email']}") print(f"Username: {us['username']}\nEmail: {us['email']}")
print("Tests complete") print("Tests complete")
if __name__ == "__main__": if __name__ == "__main__":
test_planka() test_planka()