async def book(self, facility: String, trange: TimeRange) -> IDOrError: facility = facility.value try: dtrange = rpc_tr_as_dtrange(trange) except ValueError: return IDOrError("error", String("invalid time range")) try: fbid = self._bt.book(facility, dtrange.as_trange()) except (ValueError, KeyError) as e: return IDOrError("error", String(e.args[0])) tasks = [ asyncio.create_task( s.send_notification(Action.VALUES.CREATE, facility, (dtrange,)) ) for s in self._shared_with ] await asyncio.wait(tasks) return IDOrError( "id", String( f"{b2a_base64(facility.encode('utf-8'), newline=False).decode('utf-8')}-{fbid}" ), )
async def modify(self, bid: String, delta: TimeDelta) -> IDOrError: try: facility, fbid = self._split_and_validate_bid(bid.value) except (ValueError, KeyError) as e: return IDOrError("error", String(e.args[0])) try: old_dtrange = self._bt.lookup(facility, fbid).as_dtrange() td = rpc_td_as_td(delta) new_dtrange = DateTimeRange(old_dtrange.start + td, old_dtrange.end + td) except ValueError: return IDOrError( "error", String("booking alteration causes booking to go out-of-week") ) try: self._bt.modify(facility, fbid, new_dtrange.as_trange()) except ValueError: return IDOrError( "error", String("altered booking conflicts with existing booking") ) tasks = [ asyncio.create_task( s.send_notification( Action.VALUES.MODIFY, facility, (old_dtrange, new_dtrange) ) ) for s in self._shared_with ] await asyncio.wait(tasks) return IDOrError("id", bid)
async def lookup(self, bid: String) -> BookingOrError: try: facility, fbid = self._split_and_validate_bid(bid.value) except ValueError as e: return BookingOrError("error", String(e.args[0])) trange = self._bt.lookup(facility, fbid) return BookingOrError( "booking", Booking( trange=dtrange_as_rpc_tr(trange.as_dtrange()), facility=String(facility) ), )
async def swap(self, b1: String, b2: String) -> VoidOrError: try: f1, fbid1 = self._split_and_validate_bid(b1.value) f2, fbid2 = self._split_and_validate_bid(b2.value) except ValueError as e: return VoidOrError("error", String(e.args[0])) if f1 != f2: return VoidOrError( "error", String("bookings are not for the same facility") ) self._bt.swap(f1, (fbid1, fbid2)) return VoidOrError("void")
async def show_availability(self): name = await self._prompt_facility() dows = await self._prompt_dow(7) dows = ArrayDayOfWeek(map(DayOfWeek, dows)) res = await self._proxy.query_availability(String(name), dows) if "error" in res: self._print_error(str(res.value)) return clear() print( HTML( f"<b>Availability periods for</b> <ansigreen><u>{name}</u></ansigreen>:" )) def formatted_gen(): for tr in res.value: dtrange = rpc_tr_as_dtrange(tr) yield ( DayOfWeek.VALUES(dtrange.start.weekday()).name, dtrange.start_str, dtrange.end_str, ) table_data = tuple(formatted_gen()) print( tabulate( table_data, headers=("Weekday", "Start", "End"), stralign="left", tablefmt="psql", )) self._known_facilities.add(name)
async def book(self): """ Book handles the "book" command. """ name = await self._prompt_facility() print(HTML("<u>Enter <b>start</b> time</u>:")) start = await self._prompt_time() print(HTML("<u>Enter <b>end</b> time</u>")) end = await self._prompt_time() try: rpc_tr = dtrange_as_rpc_tr(DateTimeRange(start, end)) except ValueError as e: self._print_error(f"invalid time range: {e.args[0]}") return res = await self._proxy.book(String(name), rpc_tr) if "error" in res: self._print_error(str(res.value)) return clear() print( HTML(f"<ansigreen>Successfully</ansigreen> booked {name}." f" Confirmation ID: <b><u>{res.value}</u></b>."))
async def swap(self): """ swap handles the "swap" command. """ print(HTML("<u>Enter <b>first</b> booking</u>:")) bid1 = await self._prompt_bid() print(HTML("<u>Enter <b>second</b> booking</u>")) bid2 = await self._prompt_bid() res = await self._proxy.swap(String(bid1), String(bid2)) if "error" in res: self._print_error(str(res.value)) return clear() print( HTML( f"<ansigreen>Successfully</ansigreen> swapped bookings <b><u>{bid1}</u></b> and <b><u>{bid2}</u></b>." ))
async def register_notification( self, port: u32, key: u64, facility: String, seconds: u32 ) -> VoidOrError: if port.value > 0xFFFF: return VoidOrError("error", String("invalid port number")) facility = str(facility) if facility not in self._bt.facilities: return VoidOrError("error", String(f"facility {facility} does not exist")) try: saddr = (self._caddr[0], port.value) c, p = await asyncio.wait_for( create_and_connect_client(saddr, BookingNotificationServerProxy), 10 ) except asyncio.TimeoutError: return VoidOrError("error", String("timeout while connecting to server")) # Notifications are best-effort only. c.timeout = 0 c.retries = 0 key = int(key) # Create task to expire the connection to the notification server after the # specified monitoring interval. tsk: asyncio.Task = asyncio.create_task(asyncio.sleep(int(seconds))) ns = NotificationServer(client=c, proxy=p, facility=facility, key=key, task=tsk) self._notification_servers.add(ns) def callback(_: asyncio.Task): ns.client.close() self._notification_servers.remove(ns) tsk.add_done_callback(callback) return VoidOrError("void")
async def modify(self): """ modify handles the "modify" command. """ bid = await self._prompt_bid() shift = await self._prompt_timedelta() res = await self._proxy.modify(String(bid), shift) if "error" in res: self._print_error(str(res.value)) return clear() print( HTML(f"<ansigreen>Successfully</ansigreen> modified booking" f" <b><u>{res.value}</u></b>."))
async def cancel(self): """ cancel handles the "cancel" command. """ bid = await self._prompt_bid() res = await self._proxy.cancel(String(bid)) if "error" in res: self._print_error(str(res.value)) return clear() print( HTML( f"<ansigreen>Successfully</ansigreen> canceled booking <u>{bid}</u>" ))
async def lookup(self): """ lookup handles the "lookup" command. """ bid = await self._prompt_bid() res = await self._proxy.lookup(String(bid)) if "error" in res: self._print_error(str(res.value)) return booking = res["booking"] facility = str(booking["facility"]) dtrange = rpc_tr_as_dtrange(booking["trange"]) clear() print( HTML( f"Booking <b><u>{bid}</u></b> <ansigreen>exists</ansigreen> and corresponds to the time period\n" f"<i>{dtrange}</i> for facility <b>{facility}</b>."))
async def cancel(self, bid: String) -> IDOrError: try: facility, fbid = self._split_and_validate_bid(bid.value) except ValueError as e: return IDOrError("error", String(e.args[0])) trange = self._bt.lookup(facility, fbid) self._bt.release(facility, fbid) tasks = [ asyncio.create_task( s.send_notification( Action.VALUES.RELEASE, facility, (trange.as_dtrange(),) ) ) for s in self._shared_with ] await asyncio.wait(tasks) return IDOrError("id", bid)
async def register_notifications(self): """ register_notifications handles the "register" command. """ facility = await self._prompt_facility() monitoring_time = await self._prompt_monitoring_time() res = await self._proxy.register_notification(u32(self._cbport), u64(0), String(facility), monitoring_time) if "error" in res: self._print_error(res.value) return self._known_facilities.add(facility) clear() print( HTML( f"<ansigreen>Successfully</ansigreen> registered for notifications regarding <u>{facility}</u>." ))
async def send_notification( self, action: Action.VALUES, facility: str, dtranges: Sequence[DateTimeRange] ): """ Send notifications to notification servers regarding booking actions. :param facility: facility name. :param action: booking action performed. :param dtranges: time ranges involved. """ rpc_dtranges = ArrayTimeRange(map(dtrange_as_rpc_tr, dtranges)) rpc_action = Action(action) rpc_facility = String(facility) tasks = dict( ( asyncio.create_task( s.proxy.notify(u64(s.key), rpc_action, rpc_facility, rpc_dtranges) ), s, ) for s in self._notification_servers if s.facility == facility ) # We use wait() here because we don't want the "don't abandon" behavior if tasks: await asyncio.wait(tasks) # Disconnect notification servers that can't be contacted. for task, ns in tasks.items(): # Ignore invocation timeouts because notifications are best-effort only. # The connection will eventually be closed due to the inactivity timeouts. if (task.exception() is not None) and ( not isinstance(task.exception(), asyncio.TimeoutError) ): ns.task.cancel()
def test_correct_serialized_size(self, simple_string: String): assert len(simple_string.serialize()) == simple_string.size
def test_cannot_assign_wrong_type(self, simple_string: String): with pytest.raises(TypeError): simple_string.value = 0
def test_round_trip(self, simple_string: String): recovered = String.deserialize(simple_string.serialize()) assert recovered.value == simple_string.value
def simple_string() -> String: return String("CE4013")
async def query_availability( self, facility: String, days: ArrayDayOfWeek ) -> ArrayTimeRangeOrError: facility = facility.value if not len(days): return ArrayTimeRangeOrError("error", String("no days requested")) days_list: list[DayOfWeek.VALUES] = list( set(cast(DayOfWeek, d).value for d in days) ) days_list.sort(key=lambda v: v.value) # Already sorted in ascending order. try: bookings: list[DateTimeRange] = list( map(lambda t: t[1].as_dtrange(), self._bt.list_bookings(facility)) ) except KeyError as e: return ArrayTimeRangeOrError("error", String(e.args[0])) out = ArrayTimeRange() for d in days_list: start = START_DATE + timedelta(days=d.value) end = start + timedelta(days=1) def bookings_involving_today(): today_start = start today_end = end for b in bookings: start_ok = b.start < today_end end_ok = today_start < b.end if not (start_ok and end_ok): continue yield b # Iterate over bookings involving the the selected day for b in bookings_involving_today(): astart = start aend = b.start aduration = aend - astart start = b.end # Find next event if this event occupies # first chunk of today. if b.start.day != start.day: continue # Ignore 0s periods if not aduration: continue # Extract periods of time where bookings can be made. out.append(dtrange_as_rpc_tr(DateTimeRange(astart, aend))) else: # Return any availability periods left after all earlier bookings. if start < end: out.append(dtrange_as_rpc_tr(DateTimeRange(start, end))) return ArrayTimeRangeOrError("array", out)