summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorchangiinlee <changjin9792@gmail.com>2024-02-03 15:41:14 +0900
committerchangiinlee <changjin9792@gmail.com>2024-02-03 15:41:14 +0900
commit83355ee31a6aba574f6c32037c1f5afe60366282 (patch)
tree4e66655223eb13e9cea5c1b57d411d010c0e692f
parentcc57d456686c49ac8e06d94badb4a41ea4ee5631 (diff)
♻️ refact: Refactor category commands
-rw-r--r--girok/api/category.py149
-rw-r--r--girok/commands/category/command.py218
-rw-r--r--girok/commands/category/util.py17
-rw-r--r--girok/config/auth_handler.py2
-rw-r--r--girok/constants.py5
-rw-r--r--girok/girok.py2
-rw-r--r--girok/utils/display.py4
7 files changed, 343 insertions, 54 deletions
diff --git a/girok/api/category.py b/girok/api/category.py
index 4821ddb..4eef4a6 100644
--- a/girok/api/category.py
+++ b/girok/api/category.py
@@ -1,3 +1,4 @@
+from typing import Optional
from urllib.parse import urljoin
import requests
@@ -26,14 +27,24 @@ def get_all_categories() -> APIResponse:
error_message = "Failed to get categories"
return APIResponse(is_success=False, error_message=error_message)
-
+
def create_category(category_path: str, color: str) -> APIResponse:
access_token = AuthHandler.get_access_token()
+
+ category_path_list = category_path.split("/")
+ new_category_name = category_path_list[-1]
+
+ # Resolve parent category's id
+ parent_category_id_resp = get_category_id_by_path(category_path_list[:-1])
+ if not parent_category_id_resp.is_success:
+ return APIResponse(is_success=False, error_message=parent_category_id_resp.error_message)
+
+ parent_category_id = parent_category_id_resp.body["categoryId"]
resp = requests.post(
- url=urljoin(BASE_URL, "categories/path"),
+ url=urljoin(BASE_URL, "categories"),
headers={"Authorization": "Bearer " + access_token},
- json={"path": category_path.split("/"), "color": color}
+ json={"parentId": parent_category_id, "name": new_category_name, "color": color},
)
try:
@@ -42,10 +53,136 @@ def create_category(category_path: str, color: str) -> APIResponse:
except HTTPError:
try:
error_body = resp.json()
- print(error_body)
+ error_code = error_body["errorCode"]
error_message = error_body["message"]
+
+ if error_code == "DUPLICATE_CATEGORY":
+ parent_category_path_str = (
+ "/" if not category_path_list[:-1] else "/".join(category_path_list[:-1]) + "/"
+ )
+ error_message = f"Duplicate Category: '{parent_category_path_str}' already has '{new_category_name}'"
except:
error_message = "Failed to create a new category"
-
+
+ return APIResponse(is_success=False, error_message=error_message)
+
+
+def remove_category(category_path: str) -> APIResponse:
+ access_token = AuthHandler.get_access_token()
+
+ category_path_list = category_path.split("/")
+ category_id_resp = get_category_id_by_path(category_path_list)
+ if not category_id_resp.is_success:
+ return APIResponse(is_success=False, error_message=category_id_resp.error_message)
+
+ category_id = category_id_resp.body["categoryId"]
+ resp = requests.delete(
+ url=urljoin(BASE_URL, f"categories/{category_id}"),
+ headers={"Authorization": "Bearer " + access_token},
+ )
+
+ try:
+ resp.raise_for_status()
+ return APIResponse(is_success=True)
+ except HTTPError:
+ try:
+ error_body = resp.json()
+ error_message = error_body["message"]
+ except:
+ error_message = "Failed to remove a category"
+
+ return APIResponse(is_success=False, error_message=error_message)
+
+
+def update_category(category_path: str, new_name: Optional[str] = None, new_color: Optional[str] = None) -> APIResponse:
+ access_token = AuthHandler.get_access_token()
+
+ category_path_list = category_path.split("/")
+ category_id_resp = get_category_id_by_path(category_path_list)
+ if not category_id_resp.is_success:
+ return category_id_resp
+
+ category_id = category_id_resp.body["categoryId"]
+ body = {}
+ if new_name:
+ body["newName"] = new_name
+ if new_color:
+ body["color"] = new_color
+
+ resp = requests.patch(
+ url=urljoin(BASE_URL, f"categories/{category_id}"),
+ headers={"Authorization": "Bearer " + access_token},
+ json=body,
+ )
+
+ try:
+ resp.raise_for_status()
+ return APIResponse(is_success=True)
+ except HTTPError:
+ try:
+ error_body = resp.json()
+ error_message = error_body["message"]
+ except:
+ error_message = "Failed to rename a category"
+
+ return APIResponse(is_success=False, error_message=error_message)
+
+
+def move_category(path: str, new_parent_path: str) -> APIResponse:
+ # girok mvcat A/B/C D/E
+ access_token = AuthHandler.get_access_token()
+
+ path_list = path.split("/")
+ new_parent_path_list = new_parent_path.split("/") if new_parent_path else []
+
+ # 1. Get the category id
+ resp = get_category_id_by_path(path_list)
+ if not resp.is_success:
+ return resp
+ category_id = resp.body["categoryId"]
+
+ # 2. Get target parent's id
+ resp = get_category_id_by_path(new_parent_path_list)
+ if not resp.is_success:
+ return resp
+ new_parent_category_id = resp.body["categoryId"]
+
+ resp = requests.patch(
+ url=urljoin(BASE_URL, f"categories/{category_id}/parent"),
+ headers={"Authorization": "Bearer " + access_token},
+ json={"newParentId": new_parent_category_id},
+ )
+
+ try:
+ resp.raise_for_status()
+ return APIResponse(is_success=True)
+ except HTTPError:
+ try:
+ error_body = resp.json()
+ error_message = error_body["message"]
+ except:
+ error_message = "Failed to move a category"
+ return APIResponse(is_success=False, error_message=error_message)
+
+
+def get_category_id_by_path(path_list: list[str]) -> APIResponse:
+ if len(path_list) == 0:
+ return APIResponse(is_success=True, body={"categoryId": None})
+
+ access_token = AuthHandler.get_access_token()
+ resp = requests.get(
+ url=urljoin(BASE_URL, "categories/id-by-path"),
+ headers={"Authorization": "Bearer " + access_token},
+ params={"path": path_list},
+ )
+
+ try:
+ resp.raise_for_status()
+ return APIResponse(is_success=True, body=resp.json())
+ except HTTPError:
+ try:
+ error_body = resp.json()
+ error_message = error_body["message"]
+ except:
+ error_message = f"Failed to get a category id of '{path_list}'"
return APIResponse(is_success=False, error_message=error_message)
-
diff --git a/girok/commands/category/command.py b/girok/commands/category/command.py
index 8486587..a037ec0 100644
--- a/girok/commands/category/command.py
+++ b/girok/commands/category/command.py
@@ -1,17 +1,28 @@
import re
-from typing_extensions import Annotated
+
import typer
from rich import print
from rich.console import Console
from rich.markdown import Markdown
-from rich.tree import Tree
-from rich.text import Text
from rich.style import Style
+from rich.text import Text
+from rich.tree import Tree
+from typing_extensions import Annotated
import girok.api.category as category_api
-from girok.commands.category.util import display_categories_tree, get_next_category_color
-from girok.constants import DisplayBoxType, CATEGORY_COLOR_PALETTE, Emoji, DEFAULT_CATEGORY_TEXT_COLOR, DisplayArrowType
-from girok.utils.display import center_print, arrow_print
+from girok.commands.category.util import (
+ display_categories_tree,
+ display_category_color_palette,
+ get_next_category_color,
+)
+from girok.constants import (
+ CATEGORY_COLOR_PALETTE,
+ DEFAULT_CATEGORY_TEXT_COLOR,
+ DisplayArrowType,
+ DisplayBoxType,
+ Emoji,
+)
+from girok.utils.display import arrow_print, center_print
app = typer.Typer(rich_markup_mode="rich")
console = Console()
@@ -21,10 +32,11 @@ def category_callback(ctx: typer.Context, param: typer.CallbackParam, value: str
if value is None:
return None
+ if ctx.command.name == "mvcat" and param.name == "new_parent_path" and value == "/":
+ return value.rstrip("/")
+
if not re.match("^([a-zA-Z0-9]+/)*[a-zA-Z0-9]+/?$", value):
- raise typer.BadParameter(
- "[Invalid category path] Category path must be in 'xx/yy/zz format.'"
- )
+ raise typer.BadParameter("[Invalid category path] Category path must be in 'xx/yy/zz format.'")
if value.endswith("/"):
value = value[:-1]
@@ -56,48 +68,174 @@ def show_categories():
rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]",
)
def add_category(
- category_path: Annotated[str, typer.Argument(
- ...,
- help="[yellow]Category path - xx/yy/zz..[/yellow]",
- callback=category_callback,
- )],
- color: Annotated[str, typer.Option(
- "-c", "--color",
- help="[yellow]Color[/yellow] for category"
- )] = None
+ category_path: Annotated[
+ str,
+ typer.Argument(
+ ...,
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=category_callback,
+ ),
+ ],
+ color: Annotated[str, typer.Option("-c", "--color", help="[yellow]Color[/yellow] for category")] = None,
):
# Resolve color
if color:
+ if len(category_path.split("/")) != 1:
+ arrow_print("You cannot specify non top-level category color", DisplayArrowType.ERROR)
+ raise typer.Exit()
+
if color not in CATEGORY_COLOR_PALETTE:
arrow_print("Unsupported category color\n", DisplayArrowType.ERROR)
- tree = Tree("Supported category colors")
- for color_name, hex in CATEGORY_COLOR_PALETTE.items():
- circle_text = Text(text=Emoji.CIRCLE, style=Style(color=CATEGORY_COLOR_PALETTE[color_name]))
- category_name_text = Text(
- text=f"{color_name}",
- style=Style(color=DEFAULT_CATEGORY_TEXT_COLOR)
- )
- item_text = Text.assemble(circle_text, " ", category_name_text)
- tree.add(item_text)
- console.print(tree)
+ display_category_color_palette()
raise typer.Exit()
else:
- # If color is not passed, automatically assign the color from the palette
- color = get_next_category_color()
-
- print(category_path)
- print(color)
+ # If color is not passed and top-level category, automatically assign the color from the palette
+ if len(category_path.split("/")) == 1:
+ color = get_next_category_color()
+ # Create a new category
resp = category_api.create_category(category_path, color)
- print(resp.is_success)
- print(resp.error_message)
-
-
-
-
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ resp = category_api.get_all_categories()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ center_print("Event Categories", DisplayBoxType.TITLE)
+ root_categories: list[dict] = resp.body["rootCategories"]
+ display_categories_tree(root_categories, category_path)
+
+
+@app.command(
+ "rmcat",
+ help="[red]Remove[/red] a category",
+ rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]",
+)
+def remove_category(
+ category_path: Annotated[
+ str,
+ typer.Argument(
+ ...,
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=category_callback,
+ ),
+ ],
+ force_yes: Annotated[bool, typer.Option("-y", "--yes", help="[yellow]Ignore confirm message[/yellow]")] = False,
+):
+ if not force_yes:
+ confirm_rm = typer.confirm(
+ f"[WARNING] Are you sure to delete '{category_path}'?\nAll the subcategories and tasks will also be deleted."
+ )
+ if not confirm_rm:
+ raise typer.Exit()
+
+ resp = category_api.remove_category(category_path)
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ resp = category_api.get_all_categories()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ center_print("Event Categories", DisplayBoxType.TITLE)
+ root_categories: list[dict] = resp.body["rootCategories"]
+ display_categories_tree(root_categories, category_path)
+
+
+@app.command(
+ "upcat",
+ help="[green]Rename[/green] a category",
+ rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]",
+)
+def rename_category(
+ category_path: Annotated[
+ str,
+ typer.Argument(
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=category_callback,
+ ),
+ ],
+ new_name: Annotated[str, typer.Option("-n", "--name", help="[yellow]New category name[/yellow]")] = None,
+ new_color: Annotated[str, typer.Option("-c", "--color", help="[yellow]New category color[/yellow]")] = None,
+):
+ if new_name is None and new_color is None:
+ arrow_print("Please provide fields to update", DisplayArrowType.ERROR)
+ raise typer.Exit()
+
+ # Resolve color
+ if new_color:
+ if len(category_path.split("/")) != 1:
+ arrow_print("You cannot update non top-level category color", DisplayArrowType.ERROR)
+ raise typer.Exit()
+ if new_color not in CATEGORY_COLOR_PALETTE:
+ arrow_print("Unsupported category color\n", DisplayArrowType.ERROR)
+ display_category_color_palette()
+ raise typer.Exit()
-
+ resp = category_api.update_category(category_path, new_name, new_color)
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+ resp = category_api.get_all_categories()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+ center_print("Event Categories", DisplayBoxType.TITLE)
+ root_categories: list[dict] = resp.body["rootCategories"]
+ name = new_name if new_name else category_path.split("/")[-1]
+ new_path = "/".join(category_path.split("/")[:-1] + [name])
+ display_categories_tree(root_categories, new_path)
+
+@app.command(
+ "mvcat",
+ help="[yellow]Move[/yellow] a category to under category",
+ rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]",
+)
+def move_category(
+ path: Annotated[
+ str,
+ typer.Argument(
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=category_callback,
+ ),
+ ],
+ new_parent_path: Annotated[
+ str,
+ typer.Argument(
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=category_callback,
+ ),
+ ],
+):
+ resp = category_api.move_category(path, new_parent_path)
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ resp = category_api.get_all_categories()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ center_print("Event Categories", DisplayBoxType.TITLE)
+ root_categories: list[dict] = resp.body["rootCategories"]
+ new_path = "/".join(new_parent_path.split("/") + [path.split("/")[-1]])
+ display_categories_tree(root_categories, new_path)
+
+
+@app.command(
+ "colors",
+ help="[yellow]Show[/yellow] category color palette",
+ rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]",
+)
+def show_category_color_palette():
+ display_category_color_palette()
diff --git a/girok/commands/category/util.py b/girok/commands/category/util.py
index 5c427d3..5718d76 100644
--- a/girok/commands/category/util.py
+++ b/girok/commands/category/util.py
@@ -7,13 +7,14 @@ from rich.text import Text
from rich.tree import Tree
from girok.constants import (
+ CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER,
CATEGORY_COLOR_PALETTE,
+ CONFIG_PATH,
DEFAULT_CATEGORY_TEXT_COLOR,
HIGHLIGHT_CATEGORY_TEXT_COLOR,
DisplayBoxType,
Emoji,
)
-from girok.constants import CONFIG_PATH, CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER
from girok.utils.json_utils import read_json, update_json
console = Console()
@@ -80,6 +81,16 @@ def display_category_subtree(
)
+def display_category_color_palette() -> None:
+ tree = Tree("Supported category colors")
+ for color_name, hex in CATEGORY_COLOR_PALETTE.items():
+ circle_text = Text(text=Emoji.CIRCLE, style=Style(color=CATEGORY_COLOR_PALETTE[color_name]))
+ category_name_text = Text(text=f"{color_name} ({hex})", style=Style(color=DEFAULT_CATEGORY_TEXT_COLOR))
+ item_text = Text.assemble(circle_text, " ", category_name_text)
+ tree.add(item_text)
+ console.print(tree)
+
+
def get_next_category_color() -> str:
cfg = read_json(CONFIG_PATH)
@@ -87,8 +98,8 @@ def get_next_category_color() -> str:
next_category_color_idx = cfg["next_category_color_idx"]
else:
next_category_color_idx = 0
-
+
next_category_color = CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER[next_category_color_idx]
next_category_color_idx = (next_category_color_idx + 1) % len(CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER)
update_json(CONFIG_PATH, {"next_category_color_idx": next_category_color_idx})
- return next_category_color \ No newline at end of file
+ return next_category_color
diff --git a/girok/config/auth_handler.py b/girok/config/auth_handler.py
index 633f702..c70b779 100644
--- a/girok/config/auth_handler.py
+++ b/girok/config/auth_handler.py
@@ -6,7 +6,7 @@ from girok.utils.json_utils import read_json, update_json, write_json
class AuthHandler:
-
+
@classmethod
def init(cls) -> None:
# Ensure application directory exists
diff --git a/girok/constants.py b/girok/constants.py
index c316169..710b798 100644
--- a/girok/constants.py
+++ b/girok/constants.py
@@ -23,6 +23,9 @@ class CommandName:
LOGIN = "login"
LOGOUT = "logout"
+ # Category Commands
+ COLORS = "colors"
+
# Terminal display color
class DisplayBoxType(Enum):
@@ -80,7 +83,7 @@ CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER = [
"BEIGE",
"CLOUDY",
"CORN",
- "LIGHT_PINK"
+ "LIGHT_PINK",
]
DEFAULT_CATEGORY_TEXT_COLOR = "#D7C8B7"
diff --git a/girok/girok.py b/girok/girok.py
index ef6d4e5..e5296c9 100644
--- a/girok/girok.py
+++ b/girok/girok.py
@@ -31,7 +31,7 @@ def pre_command_callback(ctx: typer.Context):
AuthHandler.init()
# Utility commands
- if cmd in [CommandName.VERSION]:
+ if cmd in [CommandName.VERSION, CommandName.COLORS]:
return
"""
diff --git a/girok/utils/display.py b/girok/utils/display.py
index 7f77c14..b9aebce 100644
--- a/girok/utils/display.py
+++ b/girok/utils/display.py
@@ -6,7 +6,7 @@ from rich.console import Console
from rich.style import Style
from rich.text import Text
-from girok.constants import DisplayBoxType, DisplayArrowType
+from girok.constants import DisplayArrowType, DisplayBoxType
console = Console()
@@ -21,4 +21,4 @@ def center_print(text: str, text_type: DisplayBoxType, wrap: bool = False) -> No
def arrow_print(text: str, text_type: DisplayArrowType) -> None:
- print(f"[{text_type.value}]> {text}[/{text_type.value}]") \ No newline at end of file
+ print(f"[{text_type.value}]> {text}[/{text_type.value}]")