-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmain.py
295 lines (233 loc) · 9.38 KB
/
main.py
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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
import datetime
import logging
import os
import re
from difflib import SequenceMatcher
import arrow
import requests
from icalendar import Calendar
from slack_sdk import WebClient
# Set up the Slack app
SLACK_BOT_TOKEN = os.environ["SLACK_BOT_TOKEN"]
client = WebClient(token=SLACK_BOT_TOKEN)
# Define the ICS file URLs and local file paths
GUSTO_ICS_URL = os.getenv("GUSTO_ICS_URL")
KINHR_LOCAL_ICS_PATH = os.getenv("KINHR_ICS_PATH")
# Define the target Slack channel
SLACK_CHANNEL = os.environ["SLACK_CHANNEL"]
# Read the 'LOG_LEVEL' environment variable
log_level_str = os.environ.get('LOG_LEVEL', 'INFO') # Default to 'INFO' if not found
# Convert the log level string to the corresponding logging level
log_level = getattr(logging, log_level_str.upper(), logging.INFO)
# Set the logging level
logging.basicConfig(level=log_level)
def fetch_calendar(source, is_url=True):
try:
if is_url:
response = requests.get(source)
response.raise_for_status()
calendar = Calendar.from_ical(response.text)
else: # If the source is a local file path
with open(source, 'r') as file:
calendar = Calendar.from_ical(file.read())
return calendar
except Exception as e:
print(f"Failed to fetch calendar from source: {source}, due to {e}")
return None
def get_events(calendar):
if not calendar:
return []
# Read the 'CALENDAR_OWNER' environment variable
calendar_owner = os.environ.get('CALENDAR_OWNER', 'Calendar Owner').replace('"',
'') # Default to 'Calendar Owner' if not found
events = []
for component in calendar.walk():
if component.name == "VEVENT":
summary = component.get("summary")
description = component.get("description", "")
# Check for "anniversary" in summary or description
if "anniversary" in summary.lower() or "anniversary" in description.lower():
continue # Skip adding this event
start = arrow.get(component.get("dtstart").dt)
end = component.get("dtend")
# Check if the events have a time, if so, convert to 'US/Eastern' timezone
if isinstance(component.get('dtstart').dt, datetime.datetime):
start = start.to('US/Eastern')
if end:
end = arrow.get(end.dt)
if isinstance(end.datetime, datetime.datetime):
end = end.to('US/Eastern')
if "VALUE=DATE" in component.get("dtend").to_ical().decode():
# If end is a date (but not a datetime), subtract one day
end = end.shift(days=-1)
else:
end = start
summary = component.get("summary")
description = component.get("description", "") # Get the description, if available
# Replace 'Your' with the value of 'CALENDAR_OWNER' and 'Paid Time Off time' with ' - OOO' in the summary
modified_summary = summary.replace("Your", calendar_owner).replace("Paid Time Off time", " - OOO")
events.append({"start": start, "end": end, "summary": modified_summary, "description": description})
return events
def extract_hours(description):
"""
Extracts hours from the event description.
Args:
description (str): The description of the event.
Returns:
str: The formatted hours string if hours are found and are less than 8, otherwise None.
"""
match = re.search(r"\((\d+) hrs\)", description)
if match:
hours = int(match.group(1))
if hours < 8:
return f"\n{hours} hrs"
return None
def format_time_range(start, end):
"""
Formats the time range based on the start and end times of the event.
Args:
start (object): The start time of the event.
end (object): The end time of the event.
Returns:
str: The formatted time range string.
"""
if start.date() == end.date():
if start.time() == end.time() and start.time().hour == 0 and start.time().minute == 0:
return "\n all-day"
else:
start_str = start.format('hh:mm A')
end_str = end.format('hh:mm A')
return f"\n{start_str} - {end_str}"
else:
start_str = start.format('YYYY-MM-DD')
end_str = end.format('YYYY-MM-DD')
return f"\nfrom {start_str} to {end_str}"
def calculate_time_range(start, end, description):
"""
Calculates the time range for the event, prioritizing the hours extracted from the description.
Args:
start (object): The start time of the event.
end (object): The end time of the event.
description (str): The description of the event.
Returns:
str: The calculated time range string.
"""
time_range = extract_hours(description)
if time_range is None:
time_range = format_time_range(start, end)
return time_range
def post_todays_events_to_slack(events):
if not events:
return
# Sort events by start date and then by summary
events.sort(key=lambda x: (x['start'], x['summary']))
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Today's events:"
}
}
]
for event in events:
start = event["start"]
end = event["end"]
summary = event["summary"]
description = event.get("description", "")
logging.debug(f"Event Description: {description}")
time_range = calculate_time_range(start, end, description)
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{summary}*{time_range}"
}
})
blocks.append({"type": "divider"})
client.chat_postMessage(channel=SLACK_CHANNEL, blocks=blocks)
def post_weekly_summary_to_slack(events):
if not events:
return
# Sort events by start date and then by summary
events.sort(key=lambda x: (x['start'], x['summary']))
blocks = [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "This week's events:"
}
}
]
for event in events:
start = event["start"]
end = event["end"]
summary = event["summary"]
description = event.get("description", "")
hours_str = extract_hours(description)
if hours_str:
# Extracting integer hours from the string.
hours = int(re.search(r"\d+", hours_str).group())
if hours < 8: # event is less than 8 hours
date_str = start.format('YYYY-MM-DD')
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{summary}* on {date_str}{hours_str}"
}
})
continue # Skip to the next iteration as this event is handled.
if start.date() == end.date(): # the event occurs within a single day
date_str = start.format('YYYY-MM-DD')
if start.time() == end.time() and start.time().hour == 0 and start.time().minute == 0: # all-day event
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{summary}* on {date_str}"
}
})
else: # event with start and end times
start_str = start.format('hh:mm A')
end_str = end.format('hh:mm A')
time_range = f" from {start_str} to {end_str}"
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{summary}* on {date_str}{time_range}"
}
})
else: # event spans multiple days
start_str = start.format('YYYY-MM-DD')
end_str = end.format('YYYY-MM-DD')
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*{summary}* from {start_str} to {end_str}"
}
})
blocks.append({"type": "divider"})
client.chat_postMessage(channel=SLACK_CHANNEL, blocks=blocks)
def daily_job():
gusto_calendar = fetch_calendar(GUSTO_ICS_URL)
gusto_events = get_events(gusto_calendar)
# Post today's events
now = arrow.now('US/Eastern')
events_today = [event for event in gusto_events if (event['start'].date() <= now.date() <= event['end'].date())]
# If today is Monday, post a summary of this week's events and anniversary events from the weekend
if now.format('dddd') == 'Monday':
# Fetch events from this week
end_of_week = now.shift(days=+6)
events_this_week = [event for event in gusto_events if
now.date() <= event['start'].date() <= end_of_week.date()]
post_weekly_summary_to_slack(events_this_week)
# post today AFTER our summary
post_todays_events_to_slack(events_today)
else:
post_todays_events_to_slack(events_today)
if __name__ == "__main__":
daily_job()