summaryrefslogtreecommitdiffstats
path: root/girok/commands/task/command.py
diff options
context:
space:
mode:
Diffstat (limited to 'girok/commands/task/command.py')
-rw-r--r--girok/commands/task/command.py472
1 files changed, 472 insertions, 0 deletions
diff --git a/girok/commands/task/command.py b/girok/commands/task/command.py
new file mode 100644
index 0000000..143a171
--- /dev/null
+++ b/girok/commands/task/command.py
@@ -0,0 +1,472 @@
+import calendar
+from datetime import datetime, timedelta
+from typing import List, Optional
+
+import typer
+from rich import print
+from typing_extensions import Annotated
+
+import girok.api.category as category_api
+import girok.api.task as task_api
+from girok.commands.task.callbacks import (
+ allow_empty_category_callback,
+ date_callback,
+ datetime_callback,
+ not_allow_empty_category_callback,
+ priority_callback,
+ tags_callback,
+ valid_integer_callback,
+)
+from girok.commands.task.display import display_events_by_list, display_events_by_tree
+from girok.commands.task.entity import Category, Event, EventDate, Repetition
+from girok.commands.task.utils import decode_date_format, validate_start_end_window
+from girok.constants import EVENT_IDS_CACHE_PATH, REPETITION_TYPE, DisplayBoxType
+from girok.utils.display import center_print
+from girok.utils.json_utils import read_json
+from girok.utils.time import build_date_info, convert_date_obj_to_iso_date_str
+
+app = typer.Typer(rich_markup_mode="rich")
+
+
+@app.command(
+ "addtask",
+ help="[yellow]Add[/yellow] a new task",
+ rich_help_panel=":fire: [bold yellow1]Task Commands[/bold yellow1]",
+)
+def add_task(
+ name: Annotated[str, typer.Argument(help="Task name")],
+ start_datetime: Annotated[
+ str, typer.Option("-d", "--date", help="[yellow]Task start datetime[/yellow]", callback=datetime_callback)
+ ],
+ end_datetime: Annotated[
+ Optional[str],
+ typer.Option("-e", "--end", help="[yellow]Task end datetime[/yellow]", callback=datetime_callback),
+ ] = None,
+ repetition: Annotated[
+ Optional[str],
+ typer.Option(
+ "-r",
+ "--repetition",
+ help="[yellow]Task repetition type. One of 'daily', 'weekly', 'monthly', 'yearly'[/yellow]",
+ ),
+ ] = None,
+ category_path: Annotated[
+ Optional[str],
+ typer.Option(
+ "-c",
+ "--category",
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=allow_empty_category_callback,
+ ),
+ ] = None,
+ tags: Annotated[
+ Optional[str],
+ typer.Option(
+ "-t",
+ "--tag",
+ help="[yellow]Tags[/yellow]. Multiple tags must be provided in 'A,B,C' format.",
+ callback=tags_callback,
+ ),
+ ] = None,
+ priority: Annotated[
+ Optional[str], typer.Option("-p", "--priority", help="[yellow]Priority[/yellow]", callback=priority_callback)
+ ] = None,
+ memo: Annotated[Optional[str], typer.Option("-m", "--memo", help="[yellow]Memo[/yellow]")] = None,
+):
+ """
+ Validate time combination. The possible combinations are:
+ 1. start_date
+ 2. start_date, start_time
+ 3. start_date, end_date
+ 4. start_date, start_time, end_date, end_time
+ """
+ start_date, start_time = start_datetime
+ end_date, end_time = None, None
+ if end_datetime:
+ end_date, end_time = end_datetime
+
+ valid, err_msg = validate_start_end_window(start_date, start_time, end_date, end_time)
+ if not valid:
+ raise typer.BadParameter(err_msg)
+
+ # Convert tags to list
+ if tags:
+ tags = tags.split(",")
+
+ # Validate repetition
+ repetition_type = None
+ repetition_end_date = None
+ if repetition:
+ # Repetition is only allowed for single-day event
+ if (start_date and end_date) and (start_date != end_date):
+ raise typer.BadParameter("Repetition is only allowed for single-day event")
+
+ if "@" not in repetition: # daily
+ if repetition not in REPETITION_TYPE:
+ raise typer.BadParameter("Repetition type must be one of 'daily', 'weekly', 'monthly', 'yearly'")
+ repetition_type = repetition
+ else: # daily@5/14
+ repetition_items = repetition.split("@")
+ if len(repetition_items) != 2:
+ raise typer.BadParameter("Invalid repetition input format")
+
+ repetition_type, repetition_end_date_str = repetition_items
+ if repetition_type not in REPETITION_TYPE:
+ raise typer.BadParameter("Repetition type must be one of 'daily', 'weekly', 'monthly', 'yearly'")
+
+ valid, iso_date_str = decode_date_format(repetition_end_date_str)
+ if not valid:
+ raise typer.BadParameter("Invalid repetition end date format")
+
+ repetition_end_date = iso_date_str
+
+ # Repetition end date must be greater than start_date
+ repetition_end_date_obj = datetime.strptime(repetition_end_date, "%Y-%m-%d").date()
+ start_date_obj = datetime.strptime(start_date, "%Y-%m-%d").date()
+ if repetition_end_date_obj <= start_date_obj:
+ raise typer.BadParameter("Repetition end date must be greater than start date")
+
+ repetition_type = REPETITION_TYPE[repetition_type]
+
+ resp = task_api.create_task(
+ name=name,
+ start_date=start_date,
+ start_time=start_time,
+ end_date=end_date,
+ end_time=end_time,
+ repetition_type=repetition_type,
+ repetition_end_date=repetition_end_date,
+ category_path=category_path,
+ tags=tags,
+ priority=priority,
+ memo=memo,
+ )
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ # Display Tasks
+ created_event_id = resp.body["eventId"]
+ resp = task_api.get_all_tasks()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ events = resp.body["events"]
+ event_entities = map_to_event_entities(events)
+ display_events_by_list(event_entities, highlight_event_id=created_event_id, highlight_action="highlight")
+
+
+@app.command(
+ "showtask",
+ help="[yellow]View[/yellow] tasks with options",
+ rich_help_panel=":fire: [bold yellow1]Task Commands[/bold yellow1]",
+)
+def showtask(
+ category_path: Annotated[
+ Optional[str],
+ typer.Option(
+ "-c",
+ "--category",
+ help="[yellow]Category path - xx/yy/zz..[/yellow]",
+ callback=not_allow_empty_category_callback,
+ ),
+ ] = None,
+ tags: Annotated[
+ Optional[str],
+ typer.Option(
+ "-t",
+ "--tag",
+ help="[yellow]Tags[/yellow]. Multiple tags must be provided in 'A,B,C' format.",
+ callback=tags_callback,
+ ),
+ ] = None,
+ priority: Annotated[
+ Optional[str], typer.Option("-p", "--priority", help="[yellow]Priority[/yellow]", callback=priority_callback)
+ ] = None,
+ exact_date: Annotated[
+ Optional[str], typer.Option("-e", "--exact", help="[yellow]Exact Deadline[/yellow]", callback=date_callback)
+ ] = None,
+ within_days: Annotated[
+ Optional[int],
+ typer.Option(
+ "-d",
+ "--day",
+ help="Show tasks due [yellow]within the specified days[/yellow]",
+ callback=valid_integer_callback,
+ ),
+ ] = None,
+ within_weeks: Annotated[
+ Optional[int],
+ typer.Option(
+ "-w",
+ "--week",
+ help="Show tasks due [yellow]within the specified weeks[/yellow]",
+ callback=valid_integer_callback,
+ ),
+ ] = None,
+ within_months: Annotated[
+ Optional[int],
+ typer.Option(
+ "-m",
+ "--month",
+ help="Show tasks due [yellow]within the specified months[/yellow]",
+ callback=valid_integer_callback,
+ ),
+ ] = None,
+ within_this_week: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-tw",
+ "--this-week",
+ help="Show tasks due [yellow]within this week[/yellow]",
+ ),
+ ] = None,
+ within_next_week: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-nw",
+ "--next-week",
+ help="Show tasks due [yellow]within next week[/yellow]",
+ ),
+ ] = None,
+ within_this_month: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-tm",
+ "--this-month",
+ help="Show tasks due [yellow]within this month[/yellow]",
+ ),
+ ] = None,
+ within_next_month: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-nm",
+ "--next-month",
+ help="Show tasks due [yellow]within next month[/yellow]",
+ ),
+ ] = None,
+ today: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-tdy",
+ "--today",
+ help="Show tasks due [yellow]today[/yellow]",
+ ),
+ ] = None,
+ tomorrow: Annotated[
+ Optional[bool],
+ typer.Option(
+ "-tmr",
+ "--tomorrow",
+ help="Show tasks due [yellow]tomorrow[/yellow]",
+ ),
+ ] = None,
+ urgent: Annotated[
+ Optional[bool], typer.Option("-u", "--urgent", help="Show [yellow]urgent[/yellow] tasks (due within 3 days)")
+ ] = None,
+ tree_view: Annotated[
+ Optional[bool],
+ typer.Option(
+ '--tree',
+ help="[yellow]Show tasks in a tree view[/yellow]"
+ )
+ ] = None
+):
+ # Resolve start_date and end_date
+ """
+ 1. exact_date -> [start, end]
+ 2. within_days -> [-inf, end]
+ 3. within_weeks -> [-inf, end]
+ 4. within_months -> [-inf, month]
+ 5. within_this_week -> [-inf, end]
+ 6. within_next_week -> [-inf, end]
+ 7. within_this_month -> [-inf, end]
+ 8. within_next_month -> [-inf, end]
+ 9. today -> [start, end]
+ 10. tomorrow -> [start, end]
+ 11. urgent -> [today, end]
+ -> else -> [-inf, inf]
+ """
+ date_options_cnt = sum(
+ [
+ opt is not None
+ for opt in [
+ exact_date,
+ within_days,
+ within_weeks,
+ within_months,
+ today,
+ tomorrow,
+ within_this_week,
+ within_next_week,
+ within_this_month,
+ within_next_month,
+ ]
+ ]
+ )
+ if date_options_cnt > 1:
+ raise typer.BadParameter("You can specify only one date option.")
+
+ start_date, end_date = "2000-01-01", convert_date_obj_to_iso_date_str(datetime.now() + timedelta(days=365))
+ if exact_date:
+ start_date, end_date = exact_date, exact_date
+ if within_days:
+ end_date = convert_date_obj_to_iso_date_str(datetime.now() + timedelta(days=within_days - 1))
+ if within_weeks:
+ end_date = convert_date_obj_to_iso_date_str(datetime.now() + timedelta(days=7 * within_weeks))
+ if within_months:
+ end_date = convert_date_obj_to_iso_date_str(datetime.now() + timedelta(days=30 * within_months))
+ if within_this_week:
+ delta = 6 - datetime.today().weekday()
+ end_date = convert_date_obj_to_iso_date_str(datetime.today() + timedelta(days=delta))
+ if within_next_week:
+ next_week = datetime.today() + timedelta(days=7)
+ delta = 6 - next_week.weekday()
+ end_date = convert_date_obj_to_iso_date_str(next_week + timedelta(days=delta))
+ if within_this_month:
+ today_date = datetime.today()
+ _, last_day = calendar.monthrange(today_date.year, today_date.month)
+ end_date = convert_date_obj_to_iso_date_str(datetime(today_date.year, today_date.month, last_day))
+ if within_next_month:
+ today_date = datetime.today()
+ if today_date.month == 12:
+ first_day_next_month = datetime(today_date.year + 1, 1, 1)
+ else:
+ first_day_next_month = datetime(today_date.year, today_date.month + 1, 1)
+ _, last_day = calendar.monthrange(first_day_next_month.year, first_day_next_month.month)
+ end_date = convert_date_obj_to_iso_date_str(
+ datetime(first_day_next_month.year, first_day_next_month.month, last_day)
+ )
+ if today:
+ start_date, end_date = convert_date_obj_to_iso_date_str(datetime.now()), convert_date_obj_to_iso_date_str(
+ datetime.now()
+ )
+ if tomorrow:
+ start_date, end_date = convert_date_obj_to_iso_date_str(
+ datetime.now() + timedelta(1)
+ ), convert_date_obj_to_iso_date_str(datetime.now() + timedelta(1))
+ if urgent:
+ start_date, end_date = convert_date_obj_to_iso_date_str(datetime.now()), convert_date_obj_to_iso_date_str(
+ datetime.now() + timedelta(days=2)
+ )
+
+ # Resolve category id
+ category_id = None
+ if category_path:
+ resp = category_api.get_category_id_by_path(category_path.split("/"))
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+ category_id = resp.body["categoryId"]
+
+ if tags:
+ tags = tags.split("/")
+
+ resp = task_api.get_all_tasks(
+ start_date=start_date, end_date=end_date, category_id=category_id, priority=priority, tags=tags
+ )
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ events = resp.body["events"]
+
+ # Display Events
+ event_entities = map_to_event_entities(events)
+ center_print(build_date_info(datetime.now()), DisplayBoxType.TITLE)
+ if tree_view:
+ resp = category_api.get_all_categories()
+ if not resp.is_success:
+ center_print("Unable to fetch categories", DisplayBoxType.ERROR)
+ raise typer.Exit()
+ categories = resp.body['rootCategories']
+ display_events_by_tree(categories, event_entities)
+ else:
+ display_events_by_list(event_entities)
+
+
+@app.command(
+ "done",
+ help="[red]Delete[/red] a task",
+ rich_help_panel=":fire: [bold yellow1]Task Commands[/bold yellow1]",
+)
+def remove_event(
+ event_id: Annotated[int, typer.Argument(help="[yellow]Task ID[/yellow] to be deleted")],
+ force: Annotated[Optional[bool], typer.Option("-y", "--yes", help="Don't show the confirmation message")] = False,
+):
+ try:
+ cache = read_json(EVENT_IDS_CACHE_PATH)
+ except:
+ center_print("First type 'showtask' command to retrieve task ids", DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ if str(event_id) not in cache:
+ center_print(f"Task id {event_id} is not found. Please enter 'showtask' command to view task ids.", DisplayBoxType.ERROR)
+ raise typer.Exit()
+ cached_event = cache[str(event_id)]
+
+ target_event_id = cached_event["id"]
+ target_event_name = cached_event["name"]
+
+ if not force:
+ done_confirm = typer.confirm(f"Are you sure to delete task [{target_event_name}]?")
+ if not done_confirm:
+ raise typer.Exit()
+
+ # Display events
+ resp = task_api.get_all_tasks()
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ events = resp.body["events"]
+ event_entities = map_to_event_entities(events)
+
+ # Remove events
+ resp = task_api.remove_event(target_event_id)
+ if not resp.is_success:
+ center_print(resp.error_message, DisplayBoxType.ERROR)
+ raise typer.Exit()
+
+ center_print("Task was successfully deleted!", DisplayBoxType.SUCCESS)
+ display_events_by_list(event_entities, highlight_event_id=target_event_id, highlight_action="delete")
+
+
+def map_to_event_entities(events: List[dict]) -> List[Event]:
+ event_entities: List[Event] = []
+ for event in events:
+ event_entity = Event(
+ id=event["id"],
+ name=event["name"],
+ color_str=event["color"],
+ tags=event["tags"],
+ priority=event["priority"],
+ memo=event["memo"],
+ event_date=EventDate(
+ start_date=event["eventDate"]["startDate"],
+ start_time=event["eventDate"]["startTime"],
+ end_date=event["eventDate"]["endDate"],
+ end_time=event["eventDate"]["endTime"],
+ ),
+ repetition=Repetition(
+ repetition_type=event["repetition"]["repetitionType"],
+ repetition_end_date=event["repetition"]["repetitionEndDate"],
+ ),
+ category_path=[Category(id=c["categoryId"], name=c["categoryName"]) for c in event["categoryPath"]],
+ )
+
+ event_entities.append(event_entity)
+
+ def sort_key(event: Event):
+ # Handle None values for start_time and end_time by replacing them with "00:00"
+ start_time = event.event_date.start_time if event.event_date.start_time else "00:00"
+ end_time = event.event_date.end_time if event.event_date.end_time else "00:00"
+ # Handle None for end_date by using start_date
+ end_date = event.event_date.end_date if event.event_date.end_date else event.event_date.start_date
+
+ return (event.event_date.start_date, start_time, end_date, end_time)
+
+ event_entities.sort(key=sort_key)
+ return event_entities
+