-
Notifications
You must be signed in to change notification settings - Fork 199
Expand file tree
/
Copy pathbin_to_ics.py
More file actions
280 lines (225 loc) · 9.03 KB
/
bin_to_ics.py
File metadata and controls
280 lines (225 loc) · 9.03 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
#!/usr/bin/env python3
"""
Script to convert UK Bin Collection Data to ICS calendar file.
Takes JSON output from the bin collection data retriever and creates calendar events
for each collection date. The events are saved to an ICS file that can be imported
into calendar applications.
Features:
- Creates all-day events for bin collections by default
- Optional alarms/reminders before collection days
- Groups multiple bin collections on the same day into one event
"""
import argparse
import datetime
import json
import os
import sys
from typing import Dict, List, Optional, Union
try:
from icalendar import Calendar, Event, Alarm
except ImportError:
print("Error: Required package 'icalendar' not found.")
print("Please install it with: pip install icalendar")
sys.exit(1)
def parse_time_delta(time_str: str) -> datetime.timedelta:
"""
Parse a time string into a timedelta object.
Formats supported:
- "1d" or "1day" or "1days" for days
- "2h" or "2hour" or "2hours" for hours
- "30m" or "30min" or "30mins" or "30minutes" for minutes
Args:
time_str: String representing a time duration
Returns:
timedelta object representing the duration
"""
time_str = time_str.lower().strip()
# Handle days
if time_str.endswith('d') or time_str.endswith('day') or time_str.endswith('days'):
if time_str.endswith('days'):
value = int(time_str[:-4])
elif time_str.endswith('day'):
value = int(time_str[:-3])
else:
value = int(time_str[:-1])
return datetime.timedelta(days=value)
# Handle hours
elif time_str.endswith('h') or time_str.endswith('hour') or time_str.endswith('hours'):
if time_str.endswith('hours'):
value = int(time_str[:-5])
elif time_str.endswith('hour'):
value = int(time_str[:-4])
else:
value = int(time_str[:-1])
return datetime.timedelta(hours=value)
# Handle minutes
elif time_str.endswith('m') or time_str.endswith('min') or time_str.endswith('mins') or time_str.endswith('minutes'):
if time_str.endswith('minutes'):
value = int(time_str[:-7])
elif time_str.endswith('mins'):
value = int(time_str[:-4])
elif time_str.endswith('min'):
value = int(time_str[:-3])
else:
value = int(time_str[:-1])
return datetime.timedelta(minutes=value)
# Default to hours if no unit specified
else:
try:
value = int(time_str)
return datetime.timedelta(hours=value)
except ValueError:
raise ValueError(f"Invalid time format: {time_str}. Use format like '1d', '2h', or '30m'.")
def create_bin_calendar(
bin_data: Dict,
calendar_name: str = "Bin Collections",
alarm_times: Optional[List[datetime.timedelta]] = None,
all_day: bool = True
) -> Calendar:
"""
Create a calendar from bin collection data.
Args:
bin_data: Dictionary containing bin collection data
calendar_name: Name of the calendar
alarm_times: List of timedeltas for when reminders should trigger before the event
all_day: Whether the events should be all-day events
Returns:
Calendar object with events for each bin collection
"""
cal = Calendar()
cal.add('prodid', '-//UK Bin Collection Data//bin_to_ics.py//EN')
cal.add('version', '2.0')
cal.add('name', calendar_name)
cal.add('x-wr-calname', calendar_name)
# Process bin collection data
if 'bins' not in bin_data:
print("Error: Invalid bin data format. 'bins' key not found.")
sys.exit(1)
# Group collections by date to combine bins collected on the same day
collections_by_date = {}
for bin_info in bin_data['bins']:
if 'type' not in bin_info or 'collectionDate' not in bin_info:
continue
bin_type = bin_info['type']
collection_date_str = bin_info['collectionDate']
# Convert date string to datetime object
try:
# Expecting format DD/MM/YYYY
collection_date = datetime.datetime.strptime(collection_date_str, "%d/%m/%Y").date()
except ValueError:
print(f"Warning: Unable to parse date '{collection_date_str}'. Skipping.")
continue
# Add to collections by date
if collection_date not in collections_by_date:
collections_by_date[collection_date] = []
collections_by_date[collection_date].append(bin_type)
# Create events for each collection date
for collection_date, bin_types in collections_by_date.items():
event = Event()
# Join multiple bin types into one summary if needed
bin_types_str = ", ".join(bin_types)
# Create event summary and description
summary = f"Bin Collection: {bin_types_str}"
description = f"Collection for: {bin_types_str}"
# Add event details
event.add('summary', summary)
event.add('description', description)
# Set the event as all-day if requested
if all_day:
event.add('dtstart', collection_date)
event.add('dtend', collection_date + datetime.timedelta(days=1))
else:
# Default to 7am for non-all-day events
collection_datetime = datetime.datetime.combine(
collection_date,
datetime.time(7, 0, 0)
)
event.add('dtstart', collection_datetime)
event.add('dtend', collection_datetime + datetime.timedelta(hours=1))
# Add alarms if specified
if alarm_times:
for alarm_time in alarm_times:
alarm = create_alarm(trigger_before=alarm_time)
event.add_component(alarm)
# Generate a unique ID for the event
event_id = f"bin-collection-{collection_date.isoformat()}-{hash(bin_types_str) % 10000:04d}@ukbincollection"
event.add('uid', event_id)
# Add the event to the calendar
cal.add_component(event)
return cal
def create_alarm(trigger_before: datetime.timedelta) -> Alarm:
"""
Create an alarm component for calendar events.
Args:
trigger_before: How long before the event to trigger the alarm
Returns:
Alarm component
"""
alarm = Alarm()
alarm.add('action', 'DISPLAY')
alarm.add('description', 'Bin collection reminder')
alarm.add('trigger', -trigger_before)
return alarm
def save_calendar(calendar: Calendar, output_file: str) -> None:
"""
Save a calendar to an ICS file.
Args:
calendar: Calendar object to save
output_file: Path to save the calendar file
"""
with open(output_file, 'wb') as f:
f.write(calendar.to_ical())
print(f"Calendar saved to {output_file}")
def load_json_data(input_file: Optional[str] = None) -> Dict:
"""
Load bin collection data from JSON file or stdin.
Args:
input_file: Path to JSON file (if None, read from stdin)
Returns:
Dictionary containing bin collection data
"""
if input_file:
try:
with open(input_file, 'r') as f:
return json.load(f)
except (json.JSONDecodeError, FileNotFoundError) as e:
print(f"Error reading input file: {e}")
sys.exit(1)
else:
try:
return json.load(sys.stdin)
except json.JSONDecodeError as e:
print(f"Error parsing JSON from stdin: {e}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description='Convert UK Bin Collection Data to ICS calendar file.')
parser.add_argument('--input', '-i', help='Input JSON file (if not provided, read from stdin)')
parser.add_argument('--output', '-o', help='Output ICS file (default: bin.ics)',
default='bin.ics')
parser.add_argument('--name', '-n', help='Calendar name (default: Bin Collections)',
default='Bin Collections')
parser.add_argument('--alarms', '-a', help='Comma-separated list of alarm times before event (e.g., "1d,2h,30m")')
parser.add_argument('--no-all-day', action='store_true', help='Create timed events instead of all-day events')
args = parser.parse_args()
# Parse alarm times
alarm_times = None
if args.alarms:
alarm_times = []
for alarm_str in args.alarms.split(','):
try:
alarm_times.append(parse_time_delta(alarm_str.strip()))
except ValueError as e:
print(f"Warning: {e}")
# Load bin collection data
bin_data = load_json_data(args.input)
# Create calendar
calendar = create_bin_calendar(
bin_data,
args.name,
alarm_times=alarm_times,
all_day=not args.no_all_day
)
# Save calendar to file
save_calendar(calendar, args.output)
if __name__ == '__main__':
main()