w
This commit is contained in:
+200
-160
@@ -9,8 +9,8 @@ from datetime import datetime, timedelta
|
||||
|
||||
from mcp.server.fastmcp import Context
|
||||
|
||||
from owa_mcp.server import mcp, AppContext
|
||||
from owa_mcp.owa_client import OWAClient
|
||||
from owa_mcp.server import AppContext, mcp
|
||||
from owa_mcp.server_lifecycle import require_client
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ def _get_client(ctx: Context) -> OWAClient:
|
||||
# Pure helper functions (preserved from find-meeting-time.py)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _parse_freebusy_string(
|
||||
freebusy_str: str, start_time: datetime, interval_minutes: int = 30
|
||||
) -> list[tuple]:
|
||||
@@ -37,7 +38,7 @@ def _parse_freebusy_string(
|
||||
|
||||
for char in freebusy_str:
|
||||
next_time = current_time + timedelta(minutes=interval_minutes)
|
||||
if char in ['1', '2', '3', '4']: # Not free
|
||||
if char in ["1", "2", "3", "4"]: # Not free
|
||||
busy_periods.append((current_time, next_time, char))
|
||||
current_time = next_time
|
||||
|
||||
@@ -105,75 +106,82 @@ def _find_free_slots(
|
||||
|
||||
def _format_time(dt: datetime) -> str:
|
||||
"""Format datetime as HH:MM."""
|
||||
return dt.strftime('%H:%M')
|
||||
return dt.strftime("%H:%M")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Helper: get busy events via GetUserAvailability (includes recurring)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_availability_events(
|
||||
client: OWAClient, email: str, start_date, end_date
|
||||
) -> list[dict]:
|
||||
"""Get busy events via GetUserAvailability (expands recurring events)."""
|
||||
payload = {
|
||||
'__type': 'GetUserAvailabilityJsonRequest:#Exchange',
|
||||
'Header': {
|
||||
'__type': 'JsonRequestHeaders:#Exchange',
|
||||
'RequestServerVersion': 'Exchange2013',
|
||||
'TimeZoneContext': {
|
||||
'__type': 'TimeZoneContext:#Exchange',
|
||||
'TimeZoneDefinition': {
|
||||
'__type': 'TimeZoneDefinitionType:#Exchange',
|
||||
'Id': 'Russian Standard Time',
|
||||
"__type": "GetUserAvailabilityJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
},
|
||||
},
|
||||
'Body': {
|
||||
'__type': 'GetUserAvailabilityRequest:#Exchange',
|
||||
'MailboxDataArray': [{
|
||||
'__type': 'MailboxData:#Exchange',
|
||||
'Email': {'__type': 'EmailAddress:#Exchange', 'Address': email},
|
||||
'AttendeeType': 'Required',
|
||||
}],
|
||||
'FreeBusyViewOptions': {
|
||||
'__type': 'FreeBusyViewOptions:#Exchange',
|
||||
'TimeWindow': {
|
||||
'__type': 'Duration:#Exchange',
|
||||
'StartTime': f'{start_date}T00:00:00',
|
||||
'EndTime': f'{end_date + timedelta(days=1)}T00:00:00',
|
||||
"Body": {
|
||||
"__type": "GetUserAvailabilityRequest:#Exchange",
|
||||
"MailboxDataArray": [
|
||||
{
|
||||
"__type": "MailboxData:#Exchange",
|
||||
"Email": {"__type": "EmailAddress:#Exchange", "Address": email},
|
||||
"AttendeeType": "Required",
|
||||
}
|
||||
],
|
||||
"FreeBusyViewOptions": {
|
||||
"__type": "FreeBusyViewOptions:#Exchange",
|
||||
"TimeWindow": {
|
||||
"__type": "Duration:#Exchange",
|
||||
"StartTime": f"{start_date}T00:00:00",
|
||||
"EndTime": f"{end_date + timedelta(days=1)}T00:00:00",
|
||||
},
|
||||
'MergedFreeBusyIntervalInMinutes': 30,
|
||||
'RequestedView': 'DetailedMerged',
|
||||
"MergedFreeBusyIntervalInMinutes": 30,
|
||||
"RequestedView": "DetailedMerged",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
data = client.request('GetUserAvailability', payload)
|
||||
body = data.get('Body', {})
|
||||
data = client.request("GetUserAvailability", payload)
|
||||
body = data.get("Body", {})
|
||||
|
||||
events = []
|
||||
for fb_resp in body.get('FreeBusyResponseArray', []):
|
||||
fb_view = fb_resp.get('FreeBusyView', {})
|
||||
cal_events = fb_view.get('CalendarEventArray', {})
|
||||
for fb_resp in body.get("FreeBusyResponseArray", []):
|
||||
fb_view = fb_resp.get("FreeBusyView", {})
|
||||
cal_events = fb_view.get("CalendarEventArray", {})
|
||||
items = (
|
||||
cal_events.get('Items', [])
|
||||
cal_events.get("Items", [])
|
||||
if isinstance(cal_events, dict)
|
||||
else (cal_events if isinstance(cal_events, list) else [])
|
||||
)
|
||||
for event in items:
|
||||
bt = event.get('BusyType', '')
|
||||
if bt in ('Free', 'NoData'):
|
||||
bt = event.get("BusyType", "")
|
||||
if bt in ("Free", "NoData"):
|
||||
continue
|
||||
|
||||
start_str = event.get('StartTime', '')
|
||||
end_str = event.get('EndTime', '')
|
||||
start_str = event.get("StartTime", "")
|
||||
end_str = event.get("EndTime", "")
|
||||
if not start_str or not end_str:
|
||||
continue
|
||||
try:
|
||||
start = datetime.fromisoformat(start_str.replace('Z', '+00:00')).replace(tzinfo=None)
|
||||
end = datetime.fromisoformat(end_str.replace('Z', '+00:00')).replace(tzinfo=None)
|
||||
events.append({'start': start, 'end': end, 'status': bt})
|
||||
start = datetime.fromisoformat(
|
||||
start_str.replace("Z", "+00:00")
|
||||
).replace(tzinfo=None)
|
||||
end = datetime.fromisoformat(end_str.replace("Z", "+00:00")).replace(
|
||||
tzinfo=None
|
||||
)
|
||||
events.append({"start": start, "end": end, "status": bt})
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
|
||||
@@ -184,22 +192,23 @@ def _get_availability_events(
|
||||
# Helper: get calendar folder ID
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_calendar_folder_id(client: OWAClient) -> str | None:
|
||||
"""Get the calendar folder ID via GetFolder."""
|
||||
payload = {
|
||||
'__type': 'GetFolderJsonRequest:#Exchange',
|
||||
'Header': {
|
||||
'__type': 'JsonRequestHeaders:#Exchange',
|
||||
'RequestServerVersion': 'Exchange2013',
|
||||
"__type": "GetFolderJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
'Body': {
|
||||
'__type': 'GetFolderRequest:#Exchange',
|
||||
'FolderShape': {
|
||||
'__type': 'FolderResponseShape:#Exchange',
|
||||
'BaseShape': 'IdOnly',
|
||||
"Body": {
|
||||
"__type": "GetFolderRequest:#Exchange",
|
||||
"FolderShape": {
|
||||
"__type": "FolderResponseShape:#Exchange",
|
||||
"BaseShape": "IdOnly",
|
||||
},
|
||||
'FolderIds': [
|
||||
{'__type': 'DistinguishedFolderId:#Exchange', 'Id': 'calendar'}
|
||||
"FolderIds": [
|
||||
{"__type": "DistinguishedFolderId:#Exchange", "Id": "calendar"}
|
||||
],
|
||||
},
|
||||
}
|
||||
@@ -218,6 +227,7 @@ def _get_calendar_folder_id(client: OWAClient) -> str | None:
|
||||
# Helper: get own calendar events (from find-free-time.py)
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
def _get_calendar_events(
|
||||
client: OWAClient, folder_id: str, start_date, end_date
|
||||
) -> list[dict]:
|
||||
@@ -228,34 +238,32 @@ def _get_calendar_events(
|
||||
|
||||
while True:
|
||||
payload = {
|
||||
'__type': 'FindItemJsonRequest:#Exchange',
|
||||
'Header': {
|
||||
'__type': 'JsonRequestHeaders:#Exchange',
|
||||
'RequestServerVersion': 'Exchange2013',
|
||||
"__type": "FindItemJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
},
|
||||
'Body': {
|
||||
'__type': 'FindItemRequest:#Exchange',
|
||||
'ItemShape': {
|
||||
'__type': 'ItemResponseShape:#Exchange',
|
||||
'BaseShape': 'AllProperties',
|
||||
"Body": {
|
||||
"__type": "FindItemRequest:#Exchange",
|
||||
"ItemShape": {
|
||||
"__type": "ItemResponseShape:#Exchange",
|
||||
"BaseShape": "AllProperties",
|
||||
},
|
||||
'ParentFolderIds': [
|
||||
{'__type': 'FolderId:#Exchange', 'Id': folder_id}
|
||||
],
|
||||
'Traversal': 'Shallow',
|
||||
'Paging': {
|
||||
'__type': 'IndexedPageView:#Exchange',
|
||||
'BasePoint': 'Beginning',
|
||||
'Offset': offset,
|
||||
'MaxEntriesReturned': batch_size,
|
||||
"ParentFolderIds": [{"__type": "FolderId:#Exchange", "Id": folder_id}],
|
||||
"Traversal": "Shallow",
|
||||
"Paging": {
|
||||
"__type": "IndexedPageView:#Exchange",
|
||||
"BasePoint": "Beginning",
|
||||
"Offset": offset,
|
||||
"MaxEntriesReturned": batch_size,
|
||||
},
|
||||
'SortOrder': [
|
||||
"SortOrder": [
|
||||
{
|
||||
'__type': 'SortResults:#Exchange',
|
||||
'Order': 'Ascending',
|
||||
'Path': {
|
||||
'__type': 'PropertyUri:#Exchange',
|
||||
'FieldURI': 'Start',
|
||||
"__type": "SortResults:#Exchange",
|
||||
"Order": "Ascending",
|
||||
"Path": {
|
||||
"__type": "PropertyUri:#Exchange",
|
||||
"FieldURI": "Start",
|
||||
},
|
||||
}
|
||||
],
|
||||
@@ -268,29 +276,29 @@ def _get_calendar_events(
|
||||
if not items:
|
||||
break
|
||||
|
||||
folder_items = items[0].get('RootFolder', {}).get('Items', [])
|
||||
folder_items = items[0].get("RootFolder", {}).get("Items", [])
|
||||
if not folder_items:
|
||||
break
|
||||
|
||||
for item in folder_items:
|
||||
# Skip cancelled events
|
||||
if item.get('IsCancelled'):
|
||||
if item.get("IsCancelled"):
|
||||
continue
|
||||
|
||||
# Skip free/tentative slots
|
||||
fbt = item.get('FreeBusyType', 'Busy')
|
||||
if fbt in ['Free', 'NoData']:
|
||||
fbt = item.get("FreeBusyType", "Busy")
|
||||
if fbt in ["Free", "NoData"]:
|
||||
continue
|
||||
|
||||
start_str = item.get('Start', '')
|
||||
end_str = item.get('End', '')
|
||||
start_str = item.get("Start", "")
|
||||
end_str = item.get("End", "")
|
||||
|
||||
if not start_str or not end_str:
|
||||
continue
|
||||
|
||||
try:
|
||||
start = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
|
||||
end = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
|
||||
start = datetime.fromisoformat(start_str.replace("Z", "+00:00"))
|
||||
end = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
|
||||
|
||||
# Convert to naive datetime for comparison
|
||||
start = start.replace(tzinfo=None)
|
||||
@@ -300,17 +308,19 @@ def _get_calendar_events(
|
||||
if end.date() < start_date or start.date() > end_date:
|
||||
continue
|
||||
|
||||
events.append({
|
||||
'start': start,
|
||||
'end': end,
|
||||
'subject': item.get('Subject', ''),
|
||||
'status': fbt,
|
||||
})
|
||||
events.append(
|
||||
{
|
||||
"start": start,
|
||||
"end": end,
|
||||
"subject": item.get("Subject", ""),
|
||||
"status": fbt,
|
||||
}
|
||||
)
|
||||
except (ValueError, AttributeError):
|
||||
continue
|
||||
|
||||
# Check if there are more items
|
||||
is_last = items[0].get('RootFolder', {}).get('IncludesLastItemInRange', True)
|
||||
is_last = items[0].get("RootFolder", {}).get("IncludesLastItemInRange", True)
|
||||
if is_last:
|
||||
break
|
||||
|
||||
@@ -323,6 +333,7 @@ def _get_calendar_events(
|
||||
# Tool: find_free_time
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def find_free_time(
|
||||
start_date: str,
|
||||
@@ -352,8 +363,8 @@ def find_free_time(
|
||||
client = _get_client(ctx)
|
||||
|
||||
try:
|
||||
sd = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
ed = datetime.strptime(end_date, '%Y-%m-%d').date() if end_date else sd
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date() if end_date else sd
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
|
||||
@@ -365,13 +376,17 @@ def find_free_time(
|
||||
# Fallback to FindItem (misses recurring event occurrences)
|
||||
folder_id = _get_calendar_folder_id(client)
|
||||
if not folder_id:
|
||||
return json.dumps({"error": "Could not find calendar folder. Session may have expired."})
|
||||
return json.dumps(
|
||||
{
|
||||
"error": "Could not find calendar folder. Session may have expired."
|
||||
}
|
||||
)
|
||||
all_busy = _get_calendar_events(client, folder_id, sd, ed)
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
# Convert event dicts to (start, end) tuples for _find_free_slots
|
||||
busy_periods = [(ev['start'], ev['end']) for ev in all_busy]
|
||||
busy_periods = [(ev["start"], ev["end"]) for ev in all_busy]
|
||||
|
||||
result = {}
|
||||
current_date = sd
|
||||
@@ -379,8 +394,11 @@ def find_free_time(
|
||||
# Skip weekends
|
||||
if current_date.weekday() < 5:
|
||||
free = _find_free_slots(
|
||||
busy_periods, current_date,
|
||||
start_hour, end_hour, duration_minutes,
|
||||
busy_periods,
|
||||
current_date,
|
||||
start_hour,
|
||||
end_hour,
|
||||
duration_minutes,
|
||||
)
|
||||
if free:
|
||||
result[str(current_date)] = [
|
||||
@@ -400,6 +418,7 @@ def find_free_time(
|
||||
# Tool: find_meeting_time
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def find_meeting_time(
|
||||
emails: str,
|
||||
@@ -431,13 +450,13 @@ def find_meeting_time(
|
||||
"""
|
||||
client = _get_client(ctx)
|
||||
|
||||
raw_list = [e.strip() for e in emails.split(',') if e.strip()]
|
||||
raw_list = [e.strip() for e in emails.split(",") if e.strip()]
|
||||
if not raw_list:
|
||||
return json.dumps({"error": "No email addresses provided."})
|
||||
|
||||
try:
|
||||
sd = datetime.strptime(start_date, '%Y-%m-%d').date()
|
||||
ed = datetime.strptime(end_date, '%Y-%m-%d').date() if end_date else sd
|
||||
sd = datetime.strptime(start_date, "%Y-%m-%d").date()
|
||||
ed = datetime.strptime(end_date, "%Y-%m-%d").date() if end_date else sd
|
||||
except ValueError as e:
|
||||
return json.dumps({"error": f"Invalid date format: {e}"})
|
||||
|
||||
@@ -445,12 +464,12 @@ def find_meeting_time(
|
||||
email_list = []
|
||||
resolve_errors = []
|
||||
for entry in raw_list:
|
||||
if '@' in entry:
|
||||
if "@" in entry:
|
||||
email_list.append(entry)
|
||||
else:
|
||||
resolutions = client.resolve_names(entry, full_contact=False)
|
||||
if resolutions:
|
||||
addr = resolutions[0].get('Mailbox', {}).get('EmailAddress', '')
|
||||
addr = resolutions[0].get("Mailbox", {}).get("EmailAddress", "")
|
||||
if addr:
|
||||
email_list.append(addr)
|
||||
else:
|
||||
@@ -459,46 +478,52 @@ def find_meeting_time(
|
||||
resolve_errors.append(entry)
|
||||
|
||||
if not email_list:
|
||||
return json.dumps({"error": f"Could not resolve any names to email addresses: {resolve_errors}"})
|
||||
return json.dumps(
|
||||
{
|
||||
"error": f"Could not resolve any names to email addresses: {resolve_errors}"
|
||||
}
|
||||
)
|
||||
|
||||
# Build mailbox data (reused for each day chunk)
|
||||
mailbox_data = []
|
||||
for email in email_list:
|
||||
mailbox_data.append({
|
||||
'__type': 'MailboxData:#Exchange',
|
||||
'Email': {
|
||||
'__type': 'EmailAddress:#Exchange',
|
||||
'Address': email,
|
||||
},
|
||||
'AttendeeType': 'Required',
|
||||
})
|
||||
mailbox_data.append(
|
||||
{
|
||||
"__type": "MailboxData:#Exchange",
|
||||
"Email": {
|
||||
"__type": "EmailAddress:#Exchange",
|
||||
"Address": email,
|
||||
},
|
||||
"AttendeeType": "Required",
|
||||
}
|
||||
)
|
||||
|
||||
# Query the full date range at once (API handles multi-day windows)
|
||||
payload = {
|
||||
'__type': 'GetUserAvailabilityJsonRequest:#Exchange',
|
||||
'Header': {
|
||||
'__type': 'JsonRequestHeaders:#Exchange',
|
||||
'RequestServerVersion': 'Exchange2013',
|
||||
'TimeZoneContext': {
|
||||
'__type': 'TimeZoneContext:#Exchange',
|
||||
'TimeZoneDefinition': {
|
||||
'__type': 'TimeZoneDefinitionType:#Exchange',
|
||||
'Id': 'Russian Standard Time',
|
||||
"__type": "GetUserAvailabilityJsonRequest:#Exchange",
|
||||
"Header": {
|
||||
"__type": "JsonRequestHeaders:#Exchange",
|
||||
"RequestServerVersion": "Exchange2013",
|
||||
"TimeZoneContext": {
|
||||
"__type": "TimeZoneContext:#Exchange",
|
||||
"TimeZoneDefinition": {
|
||||
"__type": "TimeZoneDefinitionType:#Exchange",
|
||||
"Id": "Russian Standard Time",
|
||||
},
|
||||
},
|
||||
},
|
||||
'Body': {
|
||||
'__type': 'GetUserAvailabilityRequest:#Exchange',
|
||||
'MailboxDataArray': mailbox_data,
|
||||
'FreeBusyViewOptions': {
|
||||
'__type': 'FreeBusyViewOptions:#Exchange',
|
||||
'TimeWindow': {
|
||||
'__type': 'Duration:#Exchange',
|
||||
'StartTime': f'{sd}T00:00:00',
|
||||
'EndTime': f'{ed + timedelta(days=1)}T00:00:00',
|
||||
"Body": {
|
||||
"__type": "GetUserAvailabilityRequest:#Exchange",
|
||||
"MailboxDataArray": mailbox_data,
|
||||
"FreeBusyViewOptions": {
|
||||
"__type": "FreeBusyViewOptions:#Exchange",
|
||||
"TimeWindow": {
|
||||
"__type": "Duration:#Exchange",
|
||||
"StartTime": f"{sd}T00:00:00",
|
||||
"EndTime": f"{ed + timedelta(days=1)}T00:00:00",
|
||||
},
|
||||
'MergedFreeBusyIntervalInMinutes': 30,
|
||||
'RequestedView': 'DetailedMerged',
|
||||
"MergedFreeBusyIntervalInMinutes": 30,
|
||||
"RequestedView": "DetailedMerged",
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -508,57 +533,69 @@ def find_meeting_time(
|
||||
except Exception as e:
|
||||
return json.dumps({"error": str(e)})
|
||||
|
||||
body = data.get('Body', {})
|
||||
if 'ErrorCode' in body:
|
||||
return json.dumps({"error": body.get('FaultMessage', 'Unknown error')})
|
||||
body = data.get("Body", {})
|
||||
if "ErrorCode" in body:
|
||||
return json.dumps({"error": body.get("FaultMessage", "Unknown error")})
|
||||
|
||||
# Parse availability responses
|
||||
all_busy = []
|
||||
attendee_info = []
|
||||
freebusy_responses = body.get('FreeBusyResponseArray', [])
|
||||
freebusy_responses = body.get("FreeBusyResponseArray", [])
|
||||
|
||||
for i, fb_resp in enumerate(freebusy_responses):
|
||||
fb_view = fb_resp.get('FreeBusyView', {})
|
||||
merged_fb = fb_view.get('MergedFreeBusy', '')
|
||||
email = email_list[i] if i < len(email_list) else f"Person {i+1}"
|
||||
fb_view = fb_resp.get("FreeBusyView", {})
|
||||
merged_fb = fb_view.get("MergedFreeBusy", "")
|
||||
email = email_list[i] if i < len(email_list) else f"Person {i + 1}"
|
||||
|
||||
if merged_fb:
|
||||
start_time = datetime.combine(sd, datetime.min.time())
|
||||
busy_periods = _parse_freebusy_string(merged_fb, start_time)
|
||||
|
||||
busy_count = sum(1 for c in merged_fb if c != '0')
|
||||
free_count = sum(1 for c in merged_fb if c == '0')
|
||||
attendee_info.append({
|
||||
"email": email,
|
||||
"busy_slots": busy_count,
|
||||
"free_slots": free_count,
|
||||
})
|
||||
busy_count = sum(1 for c in merged_fb if c != "0")
|
||||
free_count = sum(1 for c in merged_fb if c == "0")
|
||||
attendee_info.append(
|
||||
{
|
||||
"email": email,
|
||||
"busy_slots": busy_count,
|
||||
"free_slots": free_count,
|
||||
}
|
||||
)
|
||||
|
||||
all_busy.extend(busy_periods)
|
||||
else:
|
||||
# Fallback: parse CalendarEventArray
|
||||
cal_events_raw = fb_view.get('CalendarEventArray', {})
|
||||
cal_events = cal_events_raw.get('Items', []) if isinstance(cal_events_raw, dict) else (cal_events_raw if isinstance(cal_events_raw, list) else [])
|
||||
cal_events_raw = fb_view.get("CalendarEventArray", {})
|
||||
cal_events = (
|
||||
cal_events_raw.get("Items", [])
|
||||
if isinstance(cal_events_raw, dict)
|
||||
else (cal_events_raw if isinstance(cal_events_raw, list) else [])
|
||||
)
|
||||
if cal_events:
|
||||
attendee_info.append({
|
||||
"email": email,
|
||||
"calendar_events": len(cal_events),
|
||||
})
|
||||
attendee_info.append(
|
||||
{
|
||||
"email": email,
|
||||
"calendar_events": len(cal_events),
|
||||
}
|
||||
)
|
||||
for event in cal_events:
|
||||
start_str = event.get('StartTime', '')
|
||||
end_str = event.get('EndTime', '')
|
||||
start_str = event.get("StartTime", "")
|
||||
end_str = event.get("EndTime", "")
|
||||
if start_str and end_str:
|
||||
try:
|
||||
start = datetime.fromisoformat(start_str.replace('Z', '+00:00'))
|
||||
end = datetime.fromisoformat(end_str.replace('Z', '+00:00'))
|
||||
start = datetime.fromisoformat(
|
||||
start_str.replace("Z", "+00:00")
|
||||
)
|
||||
end = datetime.fromisoformat(end_str.replace("Z", "+00:00"))
|
||||
all_busy.append((start, end))
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
attendee_info.append({
|
||||
"email": email,
|
||||
"status": "no_data",
|
||||
})
|
||||
attendee_info.append(
|
||||
{
|
||||
"email": email,
|
||||
"status": "no_data",
|
||||
}
|
||||
)
|
||||
|
||||
merged_busy = _merge_busy_periods(all_busy)
|
||||
|
||||
@@ -568,8 +605,11 @@ def find_meeting_time(
|
||||
while current_date <= ed:
|
||||
if current_date.weekday() < 5: # Skip weekends
|
||||
free = _find_free_slots(
|
||||
merged_busy, current_date,
|
||||
start_hour, end_hour, duration_minutes,
|
||||
merged_busy,
|
||||
current_date,
|
||||
start_hour,
|
||||
end_hour,
|
||||
duration_minutes,
|
||||
)
|
||||
if free:
|
||||
free_by_date[str(current_date)] = [
|
||||
|
||||
Reference in New Issue
Block a user