-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathagenda_utils.py
More file actions
325 lines (268 loc) · 8.13 KB
/
agenda_utils.py
File metadata and controls
325 lines (268 loc) · 8.13 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
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
"""
File name: agenda_utils.py
Author: Luigi Saetta
Date last modified: 2025-12-12
Python Version: 3.11
Description:
Provides in‑memory storage and retrieval of calendar events.
Usage:
Import this module and call its functions, e.g.:
from agenda_utils import add_event, get_events
License:
MIT License
Notes:
Part of the MCP‑OCI integration demo.
Warnings:
This module is in development and may change.
"""
import random
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
# Internal in-memory event store
# Each event is stored as:
# {"id": int, "title": str, "start": datetime, "end": datetime, "notes": str}
EVENTS: List[Dict[str, Any]] = []
# Simple incremental ID generator for events
NEXT_ID: int = 1
def parse_dt(value: str) -> datetime:
"""
Parse a datetime in ISO8601 or common date formats.
Accepted formats:
- YYYY-MM-DDTHH:MM:SS
- YYYY-MM-DD HH:MM
- YYYY-MM-DD
Raises:
ValueError: if none of the formats match.
"""
for fmt in ("%Y-%m-%dT%H:%M:%S", "%Y-%m-%d %H:%M", "%Y-%m-%d"):
try:
return datetime.strptime(value, fmt)
except ValueError:
pass
raise ValueError(f"Invalid datetime format: {value}")
def _overlaps(
start_a: datetime, end_a: datetime, start_b: datetime, end_b: datetime
) -> bool:
"""
Return True if two intervals [start_a, end_a] and [start_b, end_b] overlap.
Overlap condition is strict on time, i.e. if one event ends exactly when
another starts, they do NOT overlap.
"""
return end_a > start_b and end_b > start_a
def _serialize_event(ev: Dict[str, Any]) -> Dict[str, Any]:
"""
Convert an internal event (with datetime) to a JSON-safe dict.
"""
return {
"id": ev["id"],
"title": ev["title"],
"start": ev["start"].isoformat(),
"end": ev["end"].isoformat(),
"notes": ev.get("notes", ""),
}
def add_event(
title: str,
start: str,
end: str,
notes: Optional[str] = None,
) -> Dict[str, Any]:
"""
Add a new event to the in-memory list.
Parameters
----------
title : str
Title of the event.
start : str
Start datetime (string). Accepted formats:
- YYYY-MM-DDTHH:MM:SS
- YYYY-MM-DD HH:MM
- YYYY-MM-DD
end : str
End datetime (string). Same formats as `start`.
Must be strictly after `start`.
notes: str, optional
Additional notes for the event.
Returns
-------
dict
JSON-safe description of the created event and conflicts:
{
"id": int,
"title": str,
"start": str, # ISO 8601
"end": str, # ISO 8601
"notes": str,
"has_conflict": bool,
"conflicts": [
{
"id": int,
"title": str,
"start": str, # ISO 8601
"end": str, # ISO 8601
"notes": str
},
...
]
}
Notes
-----
- The event is always created, even if there are conflicts.
- Raises ValueError if dates are invalid or end <= start.
"""
global NEXT_ID
start_dt = parse_dt(start)
end_dt = parse_dt(end)
if end_dt <= start_dt:
raise ValueError("end must be strictly after start")
# Identify conflicts BEFORE adding the new one
conflicts: List[Dict[str, Any]] = []
for ev in EVENTS:
if _overlaps(ev["start"], ev["end"], start_dt, end_dt):
conflicts.append(_serialize_event(ev))
# Store internal representation (with datetime objects and ID)
event_id = NEXT_ID
NEXT_ID += 1
new_event = {
"id": event_id,
"title": title,
"start": start_dt,
"end": end_dt,
"notes": notes or "",
}
EVENTS.append(new_event)
# Return JSON-safe data including conflicts
serialized = _serialize_event(new_event)
serialized.update(
{
"has_conflict": len(conflicts) > 0,
"conflicts": conflicts,
}
)
return serialized
def get_events(start: str, end: str) -> List[Dict[str, Any]]:
"""
Return all events overlapping with the given interval.
Parameters
----------
start : str
Start of the query interval (string).
Accepted formats:
- YYYY-MM-DDTHH:MM:SS
- YYYY-MM-DD HH:MM
- YYYY-MM-DD
end : str
End of the query interval (string).
Same formats as `start`.
Returns
-------
list of dict
JSON-safe events:
[
{
"id": int,
"title": str,
"start": str, # ISO 8601
"end": str, # ISO 8601
"notes": str
},
...
]
"""
start_dt = parse_dt(start)
end_dt = parse_dt(end)
results: List[Dict[str, Any]] = []
for ev in EVENTS:
if _overlaps(ev["start"], ev["end"], start_dt, end_dt):
results.append(_serialize_event(ev))
return results
def delete_event(event_id: int) -> Dict[str, Any]:
"""
Delete a single event by its unique ID.
Parameters
----------
event_id : int
The unique identifier of the event, as returned by add_event or get_events.
Returns
-------
dict
JSON-safe result object:
- deleted : bool
True if an event was removed, False otherwise.
- event : dict or None
If deleted == True, the JSON-safe representation of the deleted event:
{
"id": int,
"title": str,
"start": str, # ISO 8601
"end": str # ISO 8601
}
If deleted == False, this will be None.
- message : str
Human-readable summary.
"""
index_to_remove: Optional[int] = None
for idx, ev in enumerate(EVENTS):
if ev["id"] == event_id:
index_to_remove = idx
break
if index_to_remove is None:
return {
"deleted": False,
"event": None,
"message": f"Event with id {event_id} not found.",
}
ev = EVENTS.pop(index_to_remove)
return {
"deleted": True,
"event": _serialize_event(ev),
"message": f"Event with id {event_id} deleted.",
}
def init_random_events_for_current_week() -> List[Dict[str, Any]]:
"""
Initialize the EVENTS list with 10 random events for the current ISO week.
Returns
-------
list of dict
JSON-safe representation of the created events (including ids).
"""
# Event titles pool
titles = [
"Team Meeting",
"Client Call",
"Lunch Break",
"Planning Session",
"Review",
"Deep Work",
"Quick Sync",
"Project Demo",
"1:1 Meeting",
]
# Durations in minutes
durations = [30, 45, 60, 90]
# Determine current week's Monday
today = datetime.now()
monday = today - timedelta(days=today.weekday())
monday = monday.replace(hour=0, minute=0, second=0, microsecond=0)
created_events: List[Dict[str, Any]] = []
for _ in range(10):
# Random day offset (0 = Monday ... 6 = Sunday)
day_offset = random.randint(0, 6)
base_day = monday + timedelta(days=day_offset)
# Random start time between 08:00 and 18:00
start_hour = random.randint(8, 17) # end at 17:xx latest
start_minute = random.choice([0, 15, 30, 45])
start_dt = base_day.replace(
hour=start_hour,
minute=start_minute,
second=0,
microsecond=0,
)
duration = random.choice(durations)
end_dt = start_dt + timedelta(minutes=duration)
# Build start/end strings in ISO 8601
start_str = start_dt.isoformat()
end_str = end_dt.isoformat()
# Create event using add_event() so conflict detection works
ev = add_event(title=random.choice(titles), start=start_str, end=end_str)
created_events.append(ev)
return created_events