taskcheck 0.3.0__tar.gz → 0.3.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -15,6 +15,10 @@ Requires-Dist: rich>=13.9.4
15
15
 
16
16
  This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
17
17
 
18
+ <p align="center">
19
+ <img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
20
+ </p>
21
+
18
22
  ## Features
19
23
 
20
24
  - [x] Use arbitrarily complex time maps
@@ -3,6 +3,10 @@
3
3
 
4
4
  This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
5
5
 
6
+ <p align="center">
7
+ <img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
8
+ </p>
9
+
6
10
  ## Features
7
11
 
8
12
  - [x] Use arbitrarily complex time maps
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "taskcheck"
3
- version = "0.3.0"
3
+ version = "0.3.1"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -3,9 +3,12 @@ import json
3
3
  from datetime import datetime, timedelta
4
4
  from pathlib import Path
5
5
  import appdirs
6
+ from rich.console import Console
6
7
 
7
8
  from taskcheck.ical import ical_to_dict
8
9
 
10
+ console = Console()
11
+
9
12
  config_dir = Path(appdirs.user_config_dir("task"))
10
13
 
11
14
  # Taskwarrior status to avoid
@@ -98,7 +101,10 @@ def pdth_to_hours(duration_str):
98
101
  if "H" in duration_str:
99
102
  hours = int(duration_str.split("T")[1].split("H")[0])
100
103
  if "M" in duration_str:
101
- minutes = int(duration_str.split("H")[1].split("M")[0])
104
+ if "H" in duration_str:
105
+ minutes = int(duration_str.split("H")[1].split("M")[0])
106
+ else:
107
+ minutes = int(duration_str.split("T")[1].split("M")[0])
102
108
  return days * 24 + hours + minutes / 60
103
109
 
104
110
 
@@ -152,6 +158,8 @@ def get_long_range_time_map(
152
158
  time_map = time_maps[time_map_name].get(today_weekday)
153
159
  if time_map:
154
160
  for schedule_start, schedule_end in time_map:
161
+ schedule_start = hours_to_decimal(schedule_start)
162
+ schedule_end = hours_to_decimal(schedule_end)
155
163
  if schedule_start <= today_time <= schedule_end:
156
164
  today_used_hours += today_time - schedule_start
157
165
  break
@@ -5,6 +5,7 @@ from dataclasses import dataclass
5
5
  from datetime import datetime, timedelta
6
6
  from taskcheck.common import (
7
7
  AVOID_STATUS,
8
+ console,
8
9
  get_calendars,
9
10
  get_long_range_time_map,
10
11
  get_tasks,
@@ -285,6 +286,13 @@ def update_tasks_with_scheduling_info(task_info, verbose):
285
286
  stdout=subprocess.DEVNULL,
286
287
  stderr=subprocess.DEVNULL,
287
288
  )
289
+ due = task.get("due")
290
+ end_date = datetime.strptime(end_date, "%Y-%m-%d")
291
+ if due is not None and end_date > datetime.strptime(due, "%Y%m%dT%H%M%SZ"):
292
+ console.print(
293
+ f"[red]Warning: Task {task['id']} ('{task['description']}') is not going to be completed on time.[/red]"
294
+ )
295
+
288
296
  if verbose:
289
297
  print(
290
298
  f"Updated task {task['id']} with scheduled dates {start_date} to {end_date}"
@@ -0,0 +1,212 @@
1
+ import sys
2
+ from datetime import timedelta
3
+ from datetime import datetime
4
+ import json
5
+ import re
6
+ import subprocess
7
+
8
+
9
+ from rich import print
10
+ from rich.console import Console
11
+ from rich.table import Table
12
+ from rich.text import Text
13
+ from rich.panel import Panel
14
+
15
+
16
+ def get_tasks(config, tasks, year, month, day):
17
+ regex = re.compile(rf"{year}-{month}-{day} - ([PDTHM0-9]+)")
18
+ valid_tasks = []
19
+ for task in tasks:
20
+ for line in task["scheduling"].split("\n"):
21
+ m = regex.match(line)
22
+ if m:
23
+ due = task.get("due", "")
24
+ if due:
25
+ due = datetime.strptime(due, "%Y%m%dT%H%M%SZ")
26
+ due = due - datetime.today()
27
+ valid_tasks.append(
28
+ {
29
+ "id": task["id"],
30
+ "project": task.get("project", ""),
31
+ "description": task["description"],
32
+ "scheduling_day": f"{year}-{month}-{day}",
33
+ "scheduling_hours": m.group(1),
34
+ **{
35
+ attr: task.get(attr, "")
36
+ for attr in config.get("additional_attributes", [])
37
+ },
38
+ }
39
+ )
40
+ valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
41
+ return valid_tasks
42
+
43
+
44
+ def get_taskwarrior_date(date, _retry=True):
45
+ date = subprocess.run(
46
+ ["task", "calc", date],
47
+ capture_output=True,
48
+ text=True,
49
+ )
50
+ date = date.stdout.strip()
51
+ try:
52
+ date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
53
+ except Exception as _:
54
+ if _retry:
55
+ return get_taskwarrior_date("today+" + date, False)
56
+ else:
57
+ print(
58
+ "Please provide a valid date. Check with `task calc` that your date is in the format YYYY-MM-DDTHH:MM:SS",
59
+ file=sys.stderr,
60
+ )
61
+ exit(2)
62
+ return date
63
+
64
+
65
+ def get_days_in_constraint(constraint):
66
+ current_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
67
+ constraint = get_taskwarrior_date(constraint)
68
+ while current_date <= constraint:
69
+ yield current_date.year, current_date.month, current_date.day
70
+ current_date += timedelta(days=1)
71
+
72
+
73
+ def tostring(value):
74
+ if isinstance(value, bool):
75
+ return "Yes" if value else "No"
76
+ elif isinstance(value, datetime):
77
+ return value.strftime("%Y-%m-%d %H:%M")
78
+ elif isinstance(value, str):
79
+ try:
80
+ date = datetime.strptime(value, "%Y%m%dT%H%M%SZ")
81
+ return date.strftime("%Y-%m-%d %H:%M")
82
+ except ValueError:
83
+ return value
84
+ else:
85
+ return str(value)
86
+
87
+
88
+ def get_unplanned_tasks(config, tasks):
89
+ tasks = subprocess.run(
90
+ ["task", "scheduling:", "status:pending", "export"],
91
+ capture_output=True,
92
+ text=True,
93
+ )
94
+ tasks = json.loads(tasks.stdout)
95
+ return tasks
96
+
97
+
98
+ def generate_report(config, constraint, verbose=False):
99
+ config = config["report"]
100
+ console = Console()
101
+ tasks = fetch_tasks()
102
+
103
+ for year, month, day in get_days_in_constraint(constraint):
104
+ this_day_tasks = get_tasks(config, tasks, year, month, day)
105
+
106
+ display_date_header(console, year, month, day)
107
+
108
+ display_tasks_table(console, config, this_day_tasks)
109
+
110
+ if config.get("include_unplanned"):
111
+ unplanned_tasks = get_unplanned_tasks(config, tasks)
112
+ display_unplanned_tasks(console, config, unplanned_tasks)
113
+
114
+
115
+ def fetch_tasks():
116
+ """Fetch tasks from the task manager and return them as a JSON object."""
117
+ tasks = subprocess.run(
118
+ ["task", "scheduling~.", "(", "+PENDING", "or", "+WAITING", ")", "export"],
119
+ capture_output=True,
120
+ text=True,
121
+ )
122
+ return json.loads(tasks.stdout)
123
+
124
+
125
+ def display_date_header(console, year, month, day):
126
+ """Display a date header with a calendar emoji."""
127
+ date_str = f":calendar: [bold cyan]{year}-{month}-{day}[/bold cyan]"
128
+ console.print(Panel(date_str, style="bold blue", expand=False))
129
+
130
+
131
+ def display_tasks_table(console, config, tasks):
132
+ """Display a table of tasks for a specific day."""
133
+ if tasks:
134
+ table = build_tasks_table(config, tasks)
135
+ console.print(table)
136
+ else:
137
+ console.print("[italic dim]No tasks scheduled for this day.[/italic dim]")
138
+
139
+
140
+ def build_tasks_table(config, tasks):
141
+ """Build a Rich table for displaying tasks."""
142
+ table = Table(show_header=True, header_style="bold blue")
143
+ table.add_column("Task", style="dim", width=12)
144
+ table.add_column("Project", style="dim", width=12)
145
+ table.add_column("Description")
146
+ table.add_column("Time", justify="right")
147
+ for attr in config.get("additional_attributes", []):
148
+ table.add_column(attr.capitalize(), justify="right")
149
+
150
+ for task in tasks:
151
+ task_id = f"[bold green]#{task['id']}[/bold green]"
152
+ project = task.get("project", "")
153
+ description = Text(task["description"], style="white")
154
+ hours = f"[yellow]{task['scheduling_hours']}[/yellow]"
155
+ emoji = get_task_emoji(config, task)
156
+
157
+ table.add_row(
158
+ f"{emoji} {task_id}",
159
+ project,
160
+ description,
161
+ hours,
162
+ *[
163
+ tostring(task.get(attr, ""))
164
+ for attr in config.get("additional_attributes", [])
165
+ ],
166
+ )
167
+ return table
168
+
169
+
170
+ def get_task_emoji(config, task):
171
+ """Get an emoji based on keywords in the task description."""
172
+ for keyword in config.get("emoji_keywords", []):
173
+ if keyword in task["description"].lower():
174
+ return config["emoji_keywords"][keyword]
175
+ return ":pushpin:"
176
+
177
+
178
+ def display_unplanned_tasks(console, config, tasks):
179
+ """Display unplanned tasks if any are found."""
180
+ if tasks:
181
+ table = build_unplanned_tasks_table(config, tasks)
182
+ console.print(Panel("Unplanned Tasks", style="bold blue", expand=False))
183
+ console.print(table)
184
+ else:
185
+ console.print(
186
+ Panel(
187
+ "[italic dim]No unplanned tasks found.[/italic dim]",
188
+ style="bold blue",
189
+ expand=False,
190
+ )
191
+ )
192
+
193
+
194
+ def build_unplanned_tasks_table(config, tasks):
195
+ """Build a Rich table for displaying unplanned tasks."""
196
+ table = Table(show_header=True, header_style="bold blue")
197
+ table.add_column("Task", style="dim", width=12)
198
+ table.add_column("Project", style="dim", width=12)
199
+ table.add_column("Description")
200
+
201
+ for task in tasks:
202
+ task_id = f"[bold green]#{task['id']}[/bold green]"
203
+ project = task.get("project", "")
204
+ description = Text(task["description"], style="white")
205
+ emoji = get_task_emoji(config, task)
206
+
207
+ table.add_row(
208
+ f"{emoji} {task_id}",
209
+ project,
210
+ description,
211
+ )
212
+ return table
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.3.0
3
+ Version: 0.3.1
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
@@ -15,6 +15,10 @@ Requires-Dist: rich>=13.9.4
15
15
 
16
16
  This is a taskwarrior extension checks if tasks can be completed on time, considering estimated time and working hours.
17
17
 
18
+ <p align="center">
19
+ <img src="https://github.com/user-attachments/assets/b9082701-339b-4407-b941-b613a717382c"/>
20
+ </p>
21
+
18
22
  ## Features
19
23
 
20
24
  - [x] Use arbitrarily complex time maps
@@ -1,122 +0,0 @@
1
- import sys
2
- from datetime import timedelta
3
- from datetime import datetime
4
- import json
5
- import re
6
- import subprocess
7
-
8
-
9
- from rich import print
10
- from rich.console import Console
11
- from rich.table import Table
12
- from rich.text import Text
13
- from rich.panel import Panel
14
-
15
-
16
- def get_tasks(tasks, year, month, day):
17
- regex = re.compile(rf"{year}-{month}-{day} - ([PDTHM0-9]+)")
18
- valid_tasks = []
19
- for task in tasks:
20
- for line in task["scheduling"].split("\n"):
21
- m = regex.match(line)
22
- if m:
23
- due = task.get("due", "")
24
- if due:
25
- due = datetime.strptime(due, "%Y%m%dT%H%M%SZ")
26
- due = due - datetime.today()
27
- valid_tasks.append(
28
- {
29
- "id": task["id"],
30
- "project": task.get("project", ""),
31
- "description": task["description"],
32
- "scheduling_day": f"{year}-{month}-{day}",
33
- "scheduling_hours": m.group(1),
34
- "urgency": task["urgency"],
35
- "due": due,
36
- }
37
- )
38
- valid_tasks = sorted(valid_tasks, key=lambda x: x["urgency"], reverse=True)
39
- return valid_tasks
40
-
41
-
42
- def get_taskwarrior_date(date, _retry=True):
43
- date = subprocess.run(
44
- ["task", "calc", date],
45
- capture_output=True,
46
- text=True,
47
- )
48
- date = date.stdout.strip()
49
- try:
50
- date = datetime.strptime(date, "%Y-%m-%dT%H:%M:%S")
51
- except Exception as _:
52
- if _retry:
53
- return get_taskwarrior_date("today+" + date, False)
54
- else:
55
- print(
56
- "Please provide a valid date. Check with `task calc` that your date is in the format YYYY-MM-DDTHH:MM:SS",
57
- file=sys.stderr,
58
- )
59
- exit(2)
60
- return date
61
-
62
-
63
- def get_days_in_constraint(constraint):
64
- current_date = datetime.today().replace(hour=0, minute=0, second=0, microsecond=0)
65
- constraint = get_taskwarrior_date(constraint)
66
- while current_date <= constraint:
67
- yield current_date.year, current_date.month, current_date.day
68
- current_date += timedelta(days=1)
69
-
70
-
71
- def generate_report(config, constraint, verbose=False):
72
- console = Console()
73
- tasks = subprocess.run(
74
- ["task", "scheduling~.", "export"], capture_output=True, text=True
75
- )
76
- tasks = json.loads(tasks.stdout)
77
-
78
- for year, month, day in get_days_in_constraint(constraint):
79
- this_day_tasks = get_tasks(tasks, year, month, day)
80
-
81
- # Create a colorful date header with emojis and a panel
82
- date_str = f":calendar: [bold cyan]{year}-{month}-{day}[/bold cyan]"
83
- console.print(Panel(date_str, style="bold magenta", expand=False))
84
-
85
- if this_day_tasks:
86
- table = Table(show_header=True, header_style="bold blue")
87
- table.add_column("Task", style="dim", width=12)
88
- table.add_column("Project", style="dim", width=12)
89
- table.add_column("Description")
90
- table.add_column("Time", justify="right")
91
- table.add_column("Urgency")
92
- table.add_column("Due date")
93
-
94
- for task in this_day_tasks:
95
- task_id = f"[bold green]#{task['id']}[/bold green]"
96
- project = task["project"]
97
- description = Text(task["description"], style="white")
98
- hours = f"[yellow]{task['scheduling_hours']}h[/yellow]"
99
- urgency = f"[bold]{task['urgency']}[/bold]"
100
- due = task["due"]
101
- if due != "" and due < timedelta(days=5):
102
- due = f"[red]{due.days}d[/red]"
103
- elif due != "":
104
- due = f"[green]{due.days}d[/green]"
105
-
106
- # Add an emoji based on a keyword in the description
107
- if "meeting" in task["description"].lower():
108
- emoji = ":busts_in_silhouette:"
109
- elif "review" in task["description"].lower():
110
- emoji = ":mag_right:"
111
- elif "write" in task["description"].lower():
112
- emoji = ":pencil:"
113
- else:
114
- emoji = ":pushpin:"
115
-
116
- table.add_row(
117
- f"{emoji} {task_id}", project, description, hours, urgency, due
118
- )
119
-
120
- console.print(table)
121
- else:
122
- console.print("[italic dim]No tasks scheduled for this day.[/italic dim]")
File without changes
File without changes
File without changes