summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorchangiinlee <changjin9792@gmail.com>2024-02-03 00:10:14 +0900
committerchangiinlee <changjin9792@gmail.com>2024-02-03 00:10:14 +0900
commitcc57d456686c49ac8e06d94badb4a41ea4ee5631 (patch)
tree4d87f75abe90f2308ab27d8981e440e45caa114b
parent3767aa2b4b0b297cc239fb089551b6e247a88083 (diff)
♻️ refact: Refactor showcat command
-rw-r--r--girok/api/auth.py8
-rw-r--r--girok/api/category.py27
-rw-r--r--girok/commands/auth/command.py17
-rw-r--r--girok/commands/category/command.py98
-rw-r--r--girok/commands/category/util.py88
-rw-r--r--girok/config.py1
-rw-r--r--girok/config/auth_handler.py (renamed from girok/auth_handler.py)4
-rw-r--r--girok/constants.py32
-rw-r--r--girok/girok.py6
-rw-r--r--girok/utils/display.py12
-rw-r--r--pyproject.toml12
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"