from datetime import datetime, timedelta from flask import Flask, request from .data import DataFile, DateTimeSpan, MinuteSpan DATA = DataFile("data.json") app = Flask(__name__) def get_action_at(point_in_time, schedules, overrides): for override in overrides: if override.start <= point_in_time < override.end: return override.action for span in schedules[point_in_time.weekday()]: span = DateTimeSpan.from_minutespan(point_in_time, span) if span.start <= point_in_time < span.end: return True return False def get_next_action_change(point_in_time, action, schedules, overrides): skip_schedule = False if not any(schedules.values()): # No daily schedule is set overrides = [o for o in overrides if o.action] if not overrides: # No allow overrides exist => denied indefinitely return None skip_schedule = True elif all(o == [MinuteSpan(0, 24 * 60)] for o in schedules.values()): # Daily schedule allowed 24/7 overrides = [o for o in overrides if not o.action] if not overrides: # No deny overrides exist => allowed indefinitely return None skip_schedule = True if skip_schedule: # Schedule is either empty or allows 24/7, therefore irrelevant # and only overrides are considered. for override_span in overrides: if override_span.start > point_in_time: # Nearest override didn't start yet return override_span.start if override_span.start <= point_in_time < override_span.end: # We're in override return override_span.end # No more overrides => allowed / denied indefinitely return None while True: new_point_in_time = None for override_span in overrides: if override_span.start <= point_in_time < override_span.end: # We're in override new_point_in_time = override_span.end if new_point_in_time is None: dow = point_in_time.weekday() for span in schedules[dow]: span = DateTimeSpan.from_minutespan(point_in_time, span) if span.end < point_in_time: # Skip already elapsed spans continue if span.start <= point_in_time < span.end: # We're in span allowed by schedule, get the span end new_point_in_time = span.end # Check if any upcoming override comes sooner that the span end for override_span in overrides: if override_span.start >= point_in_time and override_span.start < span.end: new_point_in_time = override_span.start break break if new_point_in_time is None: # We're in span denied by schedule, get next span start days = 0 while True: for span in schedules[(dow + days) % 7]: span = DateTimeSpan.from_minutespan(point_in_time + timedelta(days=days), span) if span.start > point_in_time: new_point_in_time = span.start # Check if any upcoming override comes sooner that the span start for override_span in overrides: if ( override_span.start >= point_in_time and override_span.start < span.start ): new_point_in_time = override_span.start break break if new_point_in_time: break days += 1 if action != get_action_at(new_point_in_time, schedules, overrides): return new_point_in_time point_in_time = new_point_in_time def update_status(status, schedules, overrides): now = datetime.now() status.action = get_action_at(now, schedules, overrides) status.until = get_next_action_change(now, status.action, schedules, overrides) status.then_until = ( get_next_action_change(status.until, not status.action, schedules, overrides) if status.until else None ) def update_schedule(schedule, span, allow): if allow: schedule.append(span) schedule.sort() i = len(schedule) - 1 while i: if schedule[i - 1].end >= schedule[i].start: # spans overlap -> merge together schedule[i - 1].start = min(schedule[i - 1].start, schedule[i].start) schedule[i - 1].end = max(schedule[i - 1].end, schedule[i].end) del schedule[i] i -= 1 else: i = len(schedule) - 1 while i >= 0: if span.start <= schedule[i].start and span.end >= schedule[i].end: # deny period larger than span -> remove span del schedule[i] elif span.start > schedule[i].start and span.end < schedule[i].end: # deny period shorter than span -> split span into two schedule.append(MinuteSpan(span.end, schedule[i].end)) schedule[i].end = span.start elif span.start <= schedule[i].start < span.end: # span starts within deny period -> move start schedule[i].start = span.end elif span.start < schedule[i].end <= span.end: # span ends within deny period -> move end schedule[i].end = span.start i -= 1 schedule.sort() def update_overrides(overrides, span): now = datetime.now() if span.end <= now: # The span is in the past, nothing to do return i = len(overrides) - 1 while i >= 0: if span.start <= overrides[i].start and span.end >= overrides[i].end: # new override period larger than old override -> remove old override del overrides[i] elif span.start > overrides[i].start and span.end < overrides[i].end: if span.start > now: # new override period shorter than old override -> split old override into two overrides.append(DateTimeSpan(span.end, overrides[i].end, overrides[i].action)) overrides[i].end = span.start else: # new override period would split the override, but the first part # has already elapsed -> move old override start overrides[i].start = span.end elif span.start <= overrides[i].start < span.end: # new override starts within old overrides period -> move old override start overrides[i].start = span.end elif span.start < overrides[i].end <= span.end: # new override ends within old overrides period -> move old override end overrides[i].end = span.start i -= 1 overrides.append(span) overrides.sort() i = len(overrides) - 1 while i: if ( overrides[i - 1].end >= overrides[i].start and overrides[i - 1].action == overrides[i].action ): # overrides of the same type overlap -> merge together overrides[i - 1].start = min(overrides[i - 1].start, overrides[i].start) overrides[i - 1].end = max(overrides[i - 1].end, overrides[i].end) del overrides[i] i -= 1 @app.get("/data") def get_data(): with DATA as data: if data.status.until and datetime.now() >= data.status.until: update_status(data.status, data.schedules, data.overrides) data.save() return data.to_dict() @app.post("/override") def post_override(): span = DateTimeSpan.from_args(**request.json) with DATA as data: update_overrides(data.overrides, span) update_status(data.status, data.schedules, data.overrides) data.save() return data.to_dict() @app.post("/delete-override") def delete_override(): span = DateTimeSpan.from_args(**request.json) with DATA as data: try: data.overrides.remove(span) update_status(data.status, data.schedules, data.overrides) data.save() except ValueError: pass return data.to_dict() @app.post("/schedule") def post_schedule(): schedule = request.json span = MinuteSpan.from_args(**schedule) with DATA as data: for day in schedule["days"]: update_schedule(data.schedules[day], span, schedule["action"]) update_status(data.status, data.schedules, data.overrides) data.save() return data.to_dict() @app.get("/fast-action/") def fast_action(minutes): with DATA as data: now = datetime.now().replace(second=0, microsecond=0) if minutes: start = now action = get_action_at(now, data.schedules, data.overrides) if action: start = get_next_action_change(now, action, data.schedules, data.overrides) end = start + timedelta(minutes=minutes) span = DateTimeSpan(start, end, True) else: span = DateTimeSpan(now, now + timedelta(minutes=5), False) update_overrides(data.overrides, span) update_status(data.status, data.schedules, data.overrides) data.save() return data.to_dict()