taskcheck 0.3.0.post1__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.post1
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-1"
3
+ version = "0.3.1"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -101,7 +101,10 @@ def pdth_to_hours(duration_str):
101
101
  if "H" in duration_str:
102
102
  hours = int(duration_str.split("T")[1].split("H")[0])
103
103
  if "M" in duration_str:
104
- 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])
105
108
  return days * 24 + hours + minutes / 60
106
109
 
107
110
 
@@ -155,6 +158,8 @@ def get_long_range_time_map(
155
158
  time_map = time_maps[time_map_name].get(today_weekday)
156
159
  if time_map:
157
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)
158
163
  if schedule_start <= today_time <= schedule_end:
159
164
  today_used_hours += today_time - schedule_start
160
165
  break
@@ -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.post1
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