taskcheck 0.1.1__tar.gz → 0.1.2__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,13 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: appdirs>=1.4.4
9
9
  Requires-Dist: icalendar>=6.0.1
10
- Requires-Dist: requests-cache>=1.2.1
11
10
  Requires-Dist: requests>=2.32.3
12
11
 
13
12
 
@@ -72,6 +71,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
72
71
  [calendars.1]
73
72
  url = "https://your/url/to/calendar.ics"
74
73
  expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
74
+ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
75
75
 
76
76
  [calendars.holidays]
77
77
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
@@ -92,16 +92,21 @@ uda.completion_date.type = date
92
92
  uda.completion_date.label = Expected Completion Date
93
93
 
94
94
  # Adjust urgency for taskcheck
95
- urgency.uda.estimated.coefficient = 1.0 # Increase urgency for tasks with longer estimated durations
96
- urgency.waiting.coefficient = 0.0 # you will use `wait:` to avoid scheduling tasks too much sooner
95
+ # you will use `wait:` to avoid scheduling tasks too much sooner
96
+ urgency.waiting.coefficient = 0.0
97
+ # you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
98
+ urgency.active.coefficient = 3.0
99
+ urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
97
100
 
98
101
  # we need to be able to modify recurrent tasks without prompt
99
102
  recurrence.confirmation=no
100
103
 
101
104
  # suggested
102
- urgency.inherit=1
105
+ urgency.inherit=1 # use dependencies to automatically prioritize tasks
103
106
  urgency.blocking.coefficient=2
104
107
 
108
+ # give urgeny values to the estimated time
109
+ # this is ln(estimation)
105
110
  urgency.uda.estimated.PT1H.coefficient = 1
106
111
  urgency.uda.estimated.PT2H.coefficient = 2.32
107
112
  urgency.uda.estimated.PT3H.coefficient = 3.67
@@ -138,11 +143,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
138
143
  urgency.uda.estimated.PT34H.coefficient = 11.81
139
144
  urgency.uda.estimated.PT35H.coefficient = 11.89
140
145
  urgency.uda.estimated.PT36H.coefficient = 11.96
146
+ # maximum duration is 36 hours
147
+
141
148
  ```
142
149
 
143
150
  ## CLI Options
144
151
 
145
152
  ```
153
+
146
154
  -v, --verbose: increase output verbosity
147
155
  -t, --today: how much time has been already used today (default: 0)
156
+
148
157
  ```
@@ -60,6 +60,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
60
60
  [calendars.1]
61
61
  url = "https://your/url/to/calendar.ics"
62
62
  expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
63
+ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
63
64
 
64
65
  [calendars.holidays]
65
66
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
@@ -80,16 +81,21 @@ uda.completion_date.type = date
80
81
  uda.completion_date.label = Expected Completion Date
81
82
 
82
83
  # Adjust urgency for taskcheck
83
- urgency.uda.estimated.coefficient = 1.0 # Increase urgency for tasks with longer estimated durations
84
- urgency.waiting.coefficient = 0.0 # you will use `wait:` to avoid scheduling tasks too much sooner
84
+ # you will use `wait:` to avoid scheduling tasks too much sooner
85
+ urgency.waiting.coefficient = 0.0
86
+ # you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
87
+ urgency.active.coefficient = 3.0
88
+ urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
85
89
 
86
90
  # we need to be able to modify recurrent tasks without prompt
87
91
  recurrence.confirmation=no
88
92
 
89
93
  # suggested
90
- urgency.inherit=1
94
+ urgency.inherit=1 # use dependencies to automatically prioritize tasks
91
95
  urgency.blocking.coefficient=2
92
96
 
97
+ # give urgeny values to the estimated time
98
+ # this is ln(estimation)
93
99
  urgency.uda.estimated.PT1H.coefficient = 1
94
100
  urgency.uda.estimated.PT2H.coefficient = 2.32
95
101
  urgency.uda.estimated.PT3H.coefficient = 3.67
@@ -126,11 +132,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
126
132
  urgency.uda.estimated.PT34H.coefficient = 11.81
127
133
  urgency.uda.estimated.PT35H.coefficient = 11.89
128
134
  urgency.uda.estimated.PT36H.coefficient = 11.96
135
+ # maximum duration is 36 hours
136
+
129
137
  ```
130
138
 
131
139
  ## CLI Options
132
140
 
133
141
  ```
142
+
134
143
  -v, --verbose: increase output verbosity
135
144
  -t, --today: how much time has been already used today (default: 0)
145
+
136
146
  ```
@@ -1,15 +1,10 @@
1
1
  [project]
2
2
  name = "taskcheck"
3
- version = "0.1.1"
3
+ version = "0.1.2"
4
4
  description = "Automatic scheduler for taskwarrior"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
7
- dependencies = [
8
- "appdirs>=1.4.4",
9
- "icalendar>=6.0.1",
10
- "requests-cache>=1.2.1",
11
- "requests>=2.32.3",
12
- ]
7
+ dependencies = ["appdirs>=1.4.4", "icalendar>=6.0.1", "requests>=2.32.3"]
13
8
 
14
9
  [project.scripts]
15
10
  taskcheck = "taskcheck.__main__:main"
@@ -54,11 +54,11 @@ def _hours_to_decimal(hour):
54
54
 
55
55
  def _hours_to_time(hour):
56
56
  hours = int(hour)
57
- minutes = int((hour - hours) * 60)
57
+ minutes = int((hour - hours) * 100)
58
58
  return datetime.strptime(f"{hours}:{minutes}", "%H:%M").time()
59
59
 
60
60
 
61
- def _time_to_hours(time):
61
+ def _time_to_decimal(time):
62
62
  return time.hour + time.minute / 60
63
63
 
64
64
 
@@ -75,6 +75,7 @@ def get_available_hours(time_map, date, calendars):
75
75
  # so let's convert them to datetime.time objects
76
76
  schedule_start = _hours_to_time(schedule_start)
77
77
  schedule_end = _hours_to_time(schedule_end)
78
+ schedule_blocked_hours = 0
78
79
  for calendar in calendars:
79
80
  for event in calendar:
80
81
  # we use str to make object serializable as jsons
@@ -87,13 +88,24 @@ def get_available_hours(time_map, date, calendars):
87
88
  break
88
89
  elif event["end"].date() < date:
89
90
  continue
91
+
90
92
  # check if the event overlaps with one of the working hours
91
93
  event_start = event["start"].time()
92
94
  event_end = event["end"].time()
95
+ if event["start"].date() < date:
96
+ event_start = datetime(date.year, date.month, date.day, 0, 0).time()
97
+ if event["end"].date() > date:
98
+ event_end = datetime(date.year, date.month, date.day, 23, 59).time()
99
+
93
100
  if event_start < schedule_end and event_end > schedule_start:
94
- blocked_hours += _time_to_hours(
101
+ schedule_blocked_hours += _time_to_decimal(
95
102
  min(schedule_end, event_end)
96
- ) - _time_to_hours(max(schedule_start, event_start))
103
+ ) - _time_to_decimal(max(schedule_start, event_start))
104
+ if args.verbose and schedule_blocked_hours > 0:
105
+ print(
106
+ f"Blocked hours on {date} between {schedule_start} and {schedule_end}: {schedule_blocked_hours}"
107
+ )
108
+ blocked_hours += schedule_blocked_hours
97
109
  available_hours -= blocked_hours
98
110
  return available_hours
99
111
 
@@ -204,6 +216,7 @@ def get_calendars(config):
204
216
  all_day=calendar["event_all_day_is_blocking"],
205
217
  expiration=calendar["expiration"],
206
218
  verbose=args.verbose,
219
+ tz_name=calendar.get("timezone"),
207
220
  )
208
221
  calendar.sort(key=lambda e: e["start"])
209
222
  calendars.append(calendar)
@@ -3,38 +3,29 @@ import json
3
3
  from icalendar import Calendar
4
4
  from datetime import datetime, timedelta
5
5
  from dateutil.rrule import rruleset, rrulestr
6
- from requests_cache import CachedSession
6
+ import requests
7
7
  from pathlib import Path
8
8
  import appdirs
9
+ import zoneinfo
9
10
 
10
11
  import time
11
12
 
12
13
  CACHE = Path(appdirs.user_cache_dir("taskcheck"))
13
14
 
14
15
 
15
- def fetch_ical_data(url, expiration):
16
- session = CachedSession(
17
- "taskcheck",
18
- use_cache_dir=True, # Save files in the default user cache dir
19
- cache_control=True, # Use Cache-Control response headers for expiration, if available
20
- urls_expire_after={url: expiration}, # Set expiration time
21
- allowable_codes=[
22
- 200,
23
- ],
24
- allowable_methods=["GET", "POST"], # Cache whatever HTTP methods you want
25
- stale_if_error=True, # In case of request errors, use stale cache data if possible
26
- )
27
- response = session.get(url)
16
+ def fetch_ical_data(url):
17
+ response = requests.get(url)
28
18
  response.raise_for_status()
29
19
  return response.text
30
20
 
31
21
 
32
- def parse_ical_events(ical_text, days_ahead, all_day):
22
+ def parse_ical_events(ical_text, days_ahead, all_day, tz_name=None, verbose=False):
33
23
  cal = Calendar.from_ical(ical_text)
34
24
  today = datetime.now().date()
35
25
  end_date = today + timedelta(days=days_ahead)
36
26
 
37
27
  events = []
28
+ exceptions = set()
38
29
 
39
30
  for component in cal.walk():
40
31
  if component.name == "VEVENT":
@@ -61,9 +52,12 @@ def parse_ical_events(ical_text, days_ahead, all_day):
61
52
  )
62
53
  event_end = event_start + timedelta(days=1) - timedelta(seconds=1)
63
54
  if recurrence_id:
64
- continue # Ignore recurrence exceptions here for simplicity
55
+ if end_date >= recurrence_id.dt.date() >= today:
56
+ # this was an occurrence of a recurring event
57
+ # but has been moved, let's record the exception that must be removed
58
+ exceptions.add(recurrence_id.dt.isoformat())
65
59
 
66
- event_dates = rruleset()
60
+ occurrences = rruleset()
67
61
  if recurrence_rule:
68
62
  # avoid handling rules that are already ended
69
63
  # and don't create events beyond end_date
@@ -72,26 +66,49 @@ def parse_ical_events(ical_text, days_ahead, all_day):
72
66
  until = until[0].date()
73
67
  if until < today:
74
68
  continue
75
- elif until > end_date:
76
- recurrence_rule["UNTIL"] = end_date
77
69
  rrule = rrulestr(
78
70
  str(recurrence_rule.to_ical(), "utf-8"),
79
71
  dtstart=event_start,
80
72
  )
81
- event_dates.rrule(rrule) # type: ignore
73
+ occurrences.rrule(rrule) # type: ignore
74
+ # exceptions to recurring events
75
+ excdates = component.get("exdate")
76
+ if excdates is not None:
77
+ if isinstance(excdates, list):
78
+ # concatenate all the .dts into one list
79
+ excdates = [e for exdate in excdates for e in exdate.dts]
80
+ else:
81
+ excdates = excdates.dts
82
+ for excdate in excdates:
83
+ if end_date >= excdate.dt.date() >= today:
84
+ exceptions.add(excdate.dt.isoformat())
82
85
  else:
83
- event_dates.rdate(event_start)
84
-
85
- for event_date in event_dates:
86
- if today <= event_date.date() <= end_date:
86
+ # if event is not recurring, add it as a single event
87
+ if event_start.date() >= today and event_end.date() <= end_date:
88
+ occurrences.rdate(event_start)
89
+
90
+ for occurrence in occurrences:
91
+ if tz_name is not None:
92
+ occurrence = occurrence.astimezone(zoneinfo.ZoneInfo(tz_name))
93
+ end = occurrence + (event_end - event_start)
94
+ if occurrence.date() >= today and end.date() <= end_date:
87
95
  events.append(
88
96
  {
89
- "summary": component.get("summary"),
90
- "start": event_date.isoformat(),
91
- "end": event_end.isoformat(),
97
+ # "summary": component.get("summary"),
98
+ "start": occurrence.isoformat(),
99
+ "end": end.isoformat(),
92
100
  }
93
101
  )
102
+ elif end.date() > end_date:
103
+ break
94
104
 
105
+ with_exc_len = len(events)
106
+ events = [event for event in events if event["start"] not in exceptions]
107
+ if verbose:
108
+ print(
109
+ f"Removed {with_exc_len - len(events)} events that are exceptions to recurring events"
110
+ )
111
+ events.sort(key=lambda x: x["start"])
95
112
  return events
96
113
 
97
114
 
@@ -100,7 +117,9 @@ def get_cache_filename(url):
100
117
  return CACHE / f"{hash_object.hexdigest()}.json"
101
118
 
102
119
 
103
- def ical_to_dict(url, days_ahead=7, all_day=False, expiration=0.25, verbose=False):
120
+ def ical_to_dict(
121
+ url, days_ahead=7, all_day=False, expiration=0.25, verbose=False, tz_name=None
122
+ ):
104
123
  CACHE.mkdir(exist_ok=True, parents=True)
105
124
  cache_file = CACHE / get_cache_filename(url)
106
125
  current_time = time.time()
@@ -117,11 +136,13 @@ def ical_to_dict(url, days_ahead=7, all_day=False, expiration=0.25, verbose=Fals
117
136
 
118
137
  # Fetch and parse iCal data
119
138
  ttt = time.time()
120
- ical_text = fetch_ical_data(url, expiration * 3600)
139
+ ical_text = fetch_ical_data(url)
121
140
  if verbose:
122
141
  print("Time taken to fetch ical data: ", time.time() - ttt)
123
142
  ttt = time.time()
124
- events = parse_ical_events(ical_text, days_ahead, all_day)
143
+ events = parse_ical_events(
144
+ ical_text, days_ahead, all_day, tz_name=tz_name, verbose=verbose
145
+ )
125
146
  if verbose:
126
147
  print("Time taken to parse ical data: ", time.time() - ttt)
127
148
 
@@ -1,13 +1,12 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: taskcheck
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Automatic scheduler for taskwarrior
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
7
  License-File: LICENSE
8
8
  Requires-Dist: appdirs>=1.4.4
9
9
  Requires-Dist: icalendar>=6.0.1
10
- Requires-Dist: requests-cache>=1.2.1
11
10
  Requires-Dist: requests>=2.32.3
12
11
 
13
12
 
@@ -72,6 +71,7 @@ days_ahead = 1000 # how far go with the schedule (lower values make the computat
72
71
  [calendars.1]
73
72
  url = "https://your/url/to/calendar.ics"
74
73
  expiration = 0.08 # in hours (0.08 hours =~ 5 minutes)
74
+ timezone = "Europe/Rome" # if set, force timezone for this calendar; timezone values are TZ identifiers (https://en.wikipedia.org/wiki/List_of_tz_database_time_zones)
75
75
 
76
76
  [calendars.holidays]
77
77
  url = "https://www.officeholidays.com/ics-clean/italy/milan"
@@ -92,16 +92,21 @@ uda.completion_date.type = date
92
92
  uda.completion_date.label = Expected Completion Date
93
93
 
94
94
  # Adjust urgency for taskcheck
95
- urgency.uda.estimated.coefficient = 1.0 # Increase urgency for tasks with longer estimated durations
96
- urgency.waiting.coefficient = 0.0 # you will use `wait:` to avoid scheduling tasks too much sooner
95
+ # you will use `wait:` to avoid scheduling tasks too much sooner
96
+ urgency.waiting.coefficient = 0.0
97
+ # you will use `active:` to prioritize tasks already started even if the estimated amount is decreased
98
+ urgency.active.coefficient = 3.0
99
+ urgency.scheduled.coefficient = 0.0 # tasks that are not scheduled may still be urgent
97
100
 
98
101
  # we need to be able to modify recurrent tasks without prompt
99
102
  recurrence.confirmation=no
100
103
 
101
104
  # suggested
102
- urgency.inherit=1
105
+ urgency.inherit=1 # use dependencies to automatically prioritize tasks
103
106
  urgency.blocking.coefficient=2
104
107
 
108
+ # give urgeny values to the estimated time
109
+ # this is ln(estimation)
105
110
  urgency.uda.estimated.PT1H.coefficient = 1
106
111
  urgency.uda.estimated.PT2H.coefficient = 2.32
107
112
  urgency.uda.estimated.PT3H.coefficient = 3.67
@@ -138,11 +143,15 @@ urgency.uda.estimated.PT33H.coefficient = 11.73
138
143
  urgency.uda.estimated.PT34H.coefficient = 11.81
139
144
  urgency.uda.estimated.PT35H.coefficient = 11.89
140
145
  urgency.uda.estimated.PT36H.coefficient = 11.96
146
+ # maximum duration is 36 hours
147
+
141
148
  ```
142
149
 
143
150
  ## CLI Options
144
151
 
145
152
  ```
153
+
146
154
  -v, --verbose: increase output verbosity
147
155
  -t, --today: how much time has been already used today (default: 0)
156
+
148
157
  ```
@@ -1,4 +1,3 @@
1
1
  appdirs>=1.4.4
2
2
  icalendar>=6.0.1
3
- requests-cache>=1.2.1
4
3
  requests>=2.32.3
File without changes
File without changes