diff options
author | changiinlee <changjin9792@gmail.com> | 2024-02-03 00:10:14 +0900 |
---|---|---|
committer | changiinlee <changjin9792@gmail.com> | 2024-02-03 00:10:14 +0900 |
commit | cc57d456686c49ac8e06d94badb4a41ea4ee5631 (patch) | |
tree | 4d87f75abe90f2308ab27d8981e440e45caa114b | |
parent | 3767aa2b4b0b297cc239fb089551b6e247a88083 (diff) |
♻️ refact: Refactor showcat command
-rw-r--r-- | girok/api/auth.py | 8 | ||||
-rw-r--r-- | girok/api/category.py | 27 | ||||
-rw-r--r-- | girok/commands/auth/command.py | 17 | ||||
-rw-r--r-- | girok/commands/category/command.py | 98 | ||||
-rw-r--r-- | girok/commands/category/util.py | 88 | ||||
-rw-r--r-- | girok/config.py | 1 | ||||
-rw-r--r-- | girok/config/auth_handler.py (renamed from girok/auth_handler.py) | 4 | ||||
-rw-r--r-- | girok/constants.py | 32 | ||||
-rw-r--r-- | girok/girok.py | 6 | ||||
-rw-r--r-- | girok/utils/display.py | 12 | ||||
-rw-r--r-- | pyproject.toml | 12 |
11 files changed, 252 insertions, 53 deletions
diff --git a/girok/api/auth.py b/girok/api/auth.py index 221d5a2..1abb207 100644 --- a/girok/api/auth.py +++ b/girok/api/auth.py @@ -16,9 +16,7 @@ def verify_access_token(access_token: str) -> bool: def send_verification_code(email: str) -> APIResponse: - resp = requests.post( - url=urljoin(BASE_URL, "auth/verification-code"), json={"email": email} - ) + resp = requests.post(url=urljoin(BASE_URL, "auth/verification-code"), json={"email": email}) try: resp.raise_for_status() @@ -68,9 +66,7 @@ def register(email: str, verification_code: str, password: str) -> APIResponse: def login(email: str, password: str) -> APIResponse: - resp = requests.post( - url=urljoin(BASE_URL, "login"), json={"email": email, "password": password} - ) + resp = requests.post(url=urljoin(BASE_URL, "login"), json={"email": email, "password": password}) try: resp.raise_for_status() diff --git a/girok/api/category.py b/girok/api/category.py index 35c7b2b..4821ddb 100644 --- a/girok/api/category.py +++ b/girok/api/category.py @@ -4,7 +4,7 @@ import requests from requests import HTTPError from girok.api.entity import APIResponse -from girok.auth_handler import AuthHandler +from girok.config.auth_handler import AuthHandler from girok.constants import BASE_URL @@ -23,6 +23,29 @@ def get_all_categories() -> APIResponse: error_body = resp.json() error_message = error_body["message"] except: - error_message = "Operation failed" + 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() + resp = requests.post( + url=urljoin(BASE_URL, "categories/path"), + headers={"Authorization": "Bearer " + access_token}, + json={"path": category_path.split("/"), "color": color} + ) + + try: + resp.raise_for_status() + return APIResponse(is_success=True, body=resp.json()) + except HTTPError: + try: + error_body = resp.json() + print(error_body) + error_message = error_body["message"] + except: + error_message = "Failed to create a new category" + + return APIResponse(is_success=False, error_message=error_message) + diff --git a/girok/commands/auth/command.py b/girok/commands/auth/command.py index 9f14ed1..a7942b1 100644 --- a/girok/commands/auth/command.py +++ b/girok/commands/auth/command.py @@ -10,7 +10,7 @@ from rich.style import Style from rich.text import Text import girok.api.auth as auth_api -from girok.auth_handler import AuthHandler +from girok.config.auth_handler import AuthHandler from girok.constants import DisplayBoxType from girok.utils.display import center_print @@ -44,13 +44,9 @@ def register(): ) retry_count = 3 while retry_count > 0: - verification_code = typer.prompt( - f"> Enter verification code [{retry_count} tries left]" - ) + verification_code = typer.prompt(f"> Enter verification code [{retry_count} tries left]") retry_count -= 1 - resp = auth_api.verify_verification_code( - email=email, verification_code=verification_code - ) + resp = auth_api.verify_verification_code(email=email, verification_code=verification_code) if resp.is_success: break @@ -76,9 +72,7 @@ def register(): ) raise typer.Exit() - resp = auth_api.register( - email=email, verification_code=verification_code, password=password - ) + resp = auth_api.register(email=email, verification_code=verification_code, password=password) if not resp.is_success: center_print(resp.error_message, DisplayBoxType.ERROR) raise typer.Exit() @@ -241,7 +235,6 @@ def register_welcome(): print("\n" * 1) console.print( - " " * ((70 * screen_width) // 109 + 20) - + "Enter [green]girok login[/green] to log-in to your account." + " " * ((70 * screen_width) // 109 + 20) + "Enter [green]girok login[/green] to log-in to your account." ) print("\n" * 10) diff --git a/girok/commands/category/command.py b/girok/commands/category/command.py index 3e489e5..8486587 100644 --- a/girok/commands/category/command.py +++ b/girok/commands/category/command.py @@ -1,35 +1,103 @@ +import re +from typing_extensions import Annotated import typer from rich import print from rich.console import Console -from rich.style import Style +from rich.markdown import Markdown +from rich.tree import Tree from rich.text import Text +from rich.style import Style import girok.api.category as category_api -from girok.constants import COLOR_PALETTE, DisplayBoxType, Emoji -from girok.utils.display import center_print +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 app = typer.Typer(rich_markup_mode="rich") console = Console() +def category_callback(ctx: typer.Context, param: typer.CallbackParam, value: str): + if value is None: + return None + + 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.'" + ) + + if value.endswith("/"): + value = value[:-1] + + if value == "none": + raise typer.BadParameter("Sorry, 'none' is a reserved category name.") + return value + + @app.command( "showcat", help="[yellow]Show[/yellow] all pre-defined categories", rich_help_panel=":file_folder: [bold yellow1]Category Commands[/bold yellow1]", ) def show_categories(): - # resp = category_api.get_all_categories() - # 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) + + +@app.command( + "addcat", + help="[yellow]Add[/yellow] a new category", + 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 +): + # Resolve color + if color: + 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) + 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) + + resp = category_api.create_category(category_path, color) + print(resp.is_success) + print(resp.error_message) + + + + + + + - circle_text = Text( - text=Emoji.CIRCLE, style=Style(color=COLOR_PALETTE["GREYISH_YELLOW"]) - ) - normal_text = Text(text="Hello Category", style=Style(color="#D7C8B7", bold=True)) - console.print(Text.assemble(circle_text, " ", normal_text)) - # console.print(Text( - # text=Emoji.CIRCLE + " " + "hello" - # )) diff --git a/girok/commands/category/util.py b/girok/commands/category/util.py index 5c7cf07..5c427d3 100644 --- a/girok/commands/category/util.py +++ b/girok/commands/category/util.py @@ -1,6 +1,94 @@ +from typing import Optional + from rich import print from rich.console import Console +from rich.style import Style from rich.text import Text from rich.tree import Tree +from girok.constants import ( + CATEGORY_COLOR_PALETTE, + 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() + + +def display_categories_tree(root_categories: list[dict], highlight_category_path: Optional[str] = None): + """Display the category tree + + Args: + root_categories (list[dict]): List of top-level categories. + highlight_category_path (Optional[str], optional): Category path name to be highlighted. Must be in 'A/B/C' format. Defaults to None. + """ + tree = Tree("") + + for category in root_categories: + display_category_subtree( + tree=tree, + category=category, + highlight_category_path=highlight_category_path, + ) + console.print(tree) + + +def display_category_subtree( + tree: Tree, + category: dict, + highlight_category_path: Optional[str] = None, + parent_cumul_path: str = "", +): + """Display the subtree of a single tree node. + + Args: + tree (Tree): rich.tree.Tree object. + category (dict): A single category. It's a dictionary with keys 'id', 'name', 'color', 'children' + highlight_category_path (Optional[str], optional): Category path name to be highlighted. Must be in 'A/B/C' format. Defaults to None. + parent_cumul_path (str, optional): The cumulative category path string of the current node's parent. Defaults to "". + """ + category_name = category["name"] + category_color = category["color"] + category_children = category["children"] + current_category_path = f"{parent_cumul_path}/{category_name}".lstrip("/") # A/B/C + + circle_text = Text(text=Emoji.CIRCLE, style=Style(color=CATEGORY_COLOR_PALETTE[category_color])) + + category_name_text = Text( + text=f"{category_name}", + style=Style( + color=( + DEFAULT_CATEGORY_TEXT_COLOR + if highlight_category_path != current_category_path + else HIGHLIGHT_CATEGORY_TEXT_COLOR + ) + ), + ) + + item_text = Text.assemble(circle_text, " ", category_name_text) + sub_tree = tree.add(item_text) + for child in category_children: + display_category_subtree( + tree=sub_tree, + category=child, + highlight_category_path=highlight_category_path, + parent_cumul_path=current_category_path, + ) + + +def get_next_category_color() -> str: + cfg = read_json(CONFIG_PATH) + + if "next_category_color_idx" in cfg: + 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 diff --git a/girok/config.py b/girok/config.py deleted file mode 100644 index b4f47cb..0000000 --- a/girok/config.py +++ /dev/null @@ -1 +0,0 @@ -import typer diff --git a/girok/auth_handler.py b/girok/config/auth_handler.py index 5bb427b..633f702 100644 --- a/girok/auth_handler.py +++ b/girok/config/auth_handler.py @@ -6,9 +6,7 @@ from girok.utils.json_utils import read_json, update_json, write_json class AuthHandler: - def __init__(self): - pass - + @classmethod def init(cls) -> None: # Ensure application directory exists diff --git a/girok/constants.py b/girok/constants.py index efd7ef4..c316169 100644 --- a/girok/constants.py +++ b/girok/constants.py @@ -36,18 +36,23 @@ class DisplayBoxType(Enum): self.bg_color_hex = bg_color_hex +class DisplayArrowType(Enum): + INFO = "yellow" + ERROR = "bright_red" + + # Emojis class Emoji: LEFT_ARROW = "⬅" CIRCLE = "●" -# Color Pallete -COLOR_PALETTE = { +# Color Palette +CATEGORY_COLOR_PALETTE = { "GREYISH_YELLOW": "#F1DB76", "LIME": "#E7F8CC", "BEIGE": "#E7F8CC", - "LIGHT_PINK": "#8C0CB", + "LIGHT_PINK": "#E8C0CB", "GREYISH_GREEN": "#97C1A9", "GREYISH_BLUE": "#B0C1D5", "CLOUDY": "#94C7CB", @@ -60,4 +65,23 @@ COLOR_PALETTE = { "THOMAS": "#B19C89", } -DEFAULT_ITEM_COLOR = "#D7C8B7" +# Automatic category color assignment order +CATEGORY_COLOR_AUTO_ASSIGNMENT_ORDER = [ + "GREYISH_YELLOW", + "ROLLER_RINK", + "GREYISH_BLUE", + "GREYISH_GREEN", + "LAVENDER", + "LIME", + "LIGHT_CHOCO", + "NEON", + "THOMAS", + "MINT", + "BEIGE", + "CLOUDY", + "CORN", + "LIGHT_PINK" +] + +DEFAULT_CATEGORY_TEXT_COLOR = "#D7C8B7" +HIGHLIGHT_CATEGORY_TEXT_COLOR = "#B9D66A" diff --git a/girok/girok.py b/girok/girok.py index a685d35..ef6d4e5 100644 --- a/girok/girok.py +++ b/girok/girok.py @@ -5,7 +5,7 @@ from rich import print import girok.commands.auth.command as auth_command import girok.commands.category.command as category_command -from girok.auth_handler import AuthHandler +from girok.config.auth_handler import AuthHandler from girok.constants import VERSION, CommandName app = typer.Typer( @@ -48,9 +48,7 @@ def pre_command_callback(ctx: typer.Context): # Login required commands else: if not is_logged_in: - print( - "[red]You're not logged in. Please login with [/red][yellow]girok login[/yellow]." - ) + print("[red]You're not logged in. Please login with [/red][yellow]girok login[/yellow].") raise typer.Exit() diff --git a/girok/utils/display.py b/girok/utils/display.py index 563cb6e..7f77c14 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 +from girok.constants import DisplayBoxType, DisplayArrowType console = Console() @@ -14,11 +14,11 @@ console = Console() def center_print(text: str, text_type: DisplayBoxType, wrap: bool = False) -> None: style = Style(color=text_type.text_color_hex, bgcolor=text_type.bg_color_hex) - width = ( - shutil.get_terminal_size().columns // 2 - if wrap - else shutil.get_terminal_size().columns - ) + width = shutil.get_terminal_size().columns // 2 if wrap else shutil.get_terminal_size().columns content = Text(text, style=style) console.print(Align.center(content, style=style, width=width), height=50) + + +def arrow_print(text: str, text_type: DisplayArrowType) -> None: + print(f"[{text_type.value}]> {text}[/{text_type.value}]")
\ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 679ecc3..80ecb7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,6 +21,18 @@ ruff = "^0.2.0" black = "^24.1.1" isort = "^5.13.2" +[tool.black] +line-length = 120 +target-version = ["py39"] + +[tool.isort] +profile = "black" + +[tool.mypy] +disallow_untyped_defs = true +disallow_incomplete_defs = true +python_version = "3.9" + [build-system] requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" |