Example #1
0
    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}"
            ),
        )
Example #2
0
    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)
Example #3
0
    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)
            ),
        )
Example #4
0
    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")
Example #5
0
    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)
Example #6
0
    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>."))
Example #7
0
    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>."
            ))
Example #8
0
    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")
Example #9
0
    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>."))
Example #10
0
    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>"
            ))
Example #11
0
    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>."))
Example #12
0
    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)
Example #13
0
    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>."
            ))
Example #14
0
    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()
Example #15
0
 def test_correct_serialized_size(self, simple_string: String):
     assert len(simple_string.serialize()) == simple_string.size
Example #16
0
 def test_cannot_assign_wrong_type(self, simple_string: String):
     with pytest.raises(TypeError):
         simple_string.value = 0
Example #17
0
 def test_round_trip(self, simple_string: String):
     recovered = String.deserialize(simple_string.serialize())
     assert recovered.value == simple_string.value
Example #18
0
def simple_string() -> String:
    return String("CE4013")
Example #19
0
    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)