\chapter{The \texttt{cli.utils.rooms} module and \texttt{utils rooms} % ===> this file was generated automatically by noweave --- better not edit it
subcommands}%
\label{cli.utils.rooms}

In this chapter we introduce the subommands found under {\Tt{}nytid\ utils\ \nwnewline
rooms\nwendquote},
it's the {\Tt{}cli.utils.rooms\nwendquote} module.
We want to add tools to easily filter and find free rooms that we're interested 
in for our courses.

The overall structure is like this.
\nwfilename{rooms.nw}\nwbegincode{1}\sublabel{NWAFDpM-1T2u1E-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-1T2u1E-1}}}\moddef{rooms.py~{\nwtagstyle{}\subpageref{NWAFDpM-1T2u1E-1}}}\endmoddef\nwstartdeflinemarkup\nwenddeflinemarkup
import logging
import typer
import typerconf
import typing
from typing_extensions import Annotated

\LA{}imports~{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-1}}\RA{}

cli = typer.Typer(name="rooms",
                  help="Finding free rooms for courses")

\LA{}constants~{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-1}}\RA{}
\LA{}helper functions~{\nwtagstyle{}\subpageref{NWAFDpM-47YflN-1}}\RA{}
\LA{}argument and option definitions~{\nwtagstyle{}\subpageref{NWAFDpM-2Ns6UP-1}}\RA{}
\LA{}subcommands~{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-1}}\RA{}

if __name__ == "__main__":
  cli()
\nwnotused{rooms.py}\nwendcode{}\nwbegindocs{2}\nwdocspar


\section{Setting the URL to free rooms}

We want a command that sets the URL to the free rooms file in the config.
Something like this:
\begin{center}
{\Tt{}nytid\ utils\ rooms\ set-url\ https://cloud.timeedit.net/...\nwendquote}
\end{center}
So that we don't have to set it manually.
Then the command itself simply sets the given URL in the config.
\nwenddocs{}\nwbegincode{3}\sublabel{NWAFDpM-CyCSh-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-1}}}\moddef{subcommands~{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{\relax}{NWAFDpM-CyCSh-2}\nwenddeflinemarkup
@cli.command(name="set-url")
def set_url_cmd(\LA{}\code{}set-url\edoc{} args~{\nwtagstyle{}\subpageref{NWAFDpM-3APbeH-1}}\RA{}):
  """
  \LA{}\code{}set-url\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2wJcSK-1}}\RA{}
  """
  \LA{}update the data in the config~{\nwtagstyle{}\subpageref{NWAFDpM-nVUiQ-1}}\RA{}
\nwalsodefined{\\{NWAFDpM-CyCSh-2}\\{NWAFDpM-CyCSh-3}}\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{4}\nwdocspar

We want to add a helpful description of how to find the right URL to add.
This is another reason to make this a command.
\nwenddocs{}\nwbegincode{5}\sublabel{NWAFDpM-2wJcSK-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-2wJcSK-1}}}\moddef{\code{}set-url\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2wJcSK-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-1}}\nwenddeflinemarkup
Search for all the rooms that you're interested in in TimeEdit,
e.g. D1, D2, D3 and D37.
Set the relevant time intervals to something future proof: you
don't want it to be too short, then you have to update the URL
too often. Use a relative time frame. Get the URL for subscribing,
ICS format, make sure to select the right time frame.
\nwused{\\{NWAFDpM-CyCSh-1}}\nwendcode{}\nwbegindocs{6}\nwdocspar

To set the URL we need the URL as an argument.
\nwenddocs{}\nwbegincode{7}\sublabel{NWAFDpM-3APbeH-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3APbeH-1}}}\moddef{\code{}set-url\edoc{} args~{\nwtagstyle{}\subpageref{NWAFDpM-3APbeH-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-1}}\nwprevnextdefs{\relax}{NWAFDpM-3APbeH-2}\nwenddeflinemarkup
url: Annotated[str,
               typer.Argument(help="URL to TimeEdit export, "
                                   "ICS format for subscribing.",
                              show_default=False)],
\nwalsodefined{\\{NWAFDpM-3APbeH-2}}\nwused{\\{NWAFDpM-CyCSh-1}}\nwendcode{}\nwbegincode{8}\sublabel{NWAFDpM-nVUiQ-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-nVUiQ-1}}}\moddef{update the data in the config~{\nwtagstyle{}\subpageref{NWAFDpM-nVUiQ-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-1}}\nwprevnextdefs{\relax}{NWAFDpM-nVUiQ-2}\nwenddeflinemarkup
try:
  typerconf.set(BOOKED_ROOMS_URL, url)
except Exception as err:
  logging.error(f"Can't set URL: \{err\}")
  raise typer.Exit(1)
\nwalsodefined{\\{NWAFDpM-nVUiQ-2}}\nwused{\\{NWAFDpM-CyCSh-1}}\nwendcode{}\nwbegincode{9}\sublabel{NWAFDpM-3UrjvT-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-1}}}\moddef{constants~{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{\relax}{NWAFDpM-3UrjvT-2}\nwenddeflinemarkup
BOOKED_ROOMS_URL = "utils.rooms.url"
\nwalsodefined{\\{NWAFDpM-3UrjvT-2}\\{NWAFDpM-3UrjvT-3}}\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{10}\nwdocspar

We also want to have a list of the rooms that we searched for.
This is so that we can figure out that a room is free---\ie not in the 
ICS---when we can't find a booking for it.
We want the user to supply such a list of rooms.
\nwenddocs{}\nwbegincode{11}\sublabel{NWAFDpM-3APbeH-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3APbeH-2}}}\moddef{\code{}set-url\edoc{} args~{\nwtagstyle{}\subpageref{NWAFDpM-3APbeH-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-1}}\nwprevnextdefs{NWAFDpM-3APbeH-1}{\relax}\nwenddeflinemarkup
interesting_rooms: Annotated[typing.List[str],
                             typer.Argument(help="Space-separated list of "
                                                 "rooms in the schedule, e.g.: "
                                                 "D1 D2 D3 D37",
                                            show_default=False)]
\nwused{\\{NWAFDpM-CyCSh-1}}\nwendcode{}\nwbegincode{12}\sublabel{NWAFDpM-nVUiQ-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-nVUiQ-2}}}\moddef{update the data in the config~{\nwtagstyle{}\subpageref{NWAFDpM-nVUiQ-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-1}}\nwprevnextdefs{NWAFDpM-nVUiQ-1}{\relax}\nwenddeflinemarkup
try:
  typerconf.set(INTERESTING_ROOMS, interesting_rooms)
except Exception as err:
  logging.error(f"Can't set interesting rooms: \{err\}")
  raise typer.Exit(1)
\nwused{\\{NWAFDpM-CyCSh-1}}\nwendcode{}\nwbegincode{13}\sublabel{NWAFDpM-3UrjvT-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-2}}}\moddef{constants~{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-3UrjvT-1}{NWAFDpM-3UrjvT-3}\nwenddeflinemarkup
INTERESTING_ROOMS = "utils.rooms.interesting_rooms"
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{14}\nwdocspar


\section{Computing free rooms}

We want to write a function that computes the free rooms based on the schedule 
we get from TimeEdit.
\nwenddocs{}\nwbegincode{15}\sublabel{NWAFDpM-2sT7s4-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-2sT7s4-1}}}\moddef{\code{}free{\_}rooms\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2sT7s4-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-47YflN-1}}\nwenddeflinemarkup
Given a URL or path to a TimeEdit ICS file (`ics_url`),
return a CSV (list of tuples) of the free rooms:

[(start, end, unbooked_rooms), ...]

where start and end are datetime objects and unbooked_rooms is a set of 
strings.
\nwused{\\{NWAFDpM-47YflN-1}}\nwendcode{}\nwbegincode{16}\sublabel{NWAFDpM-47YflN-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-47YflN-1}}}\moddef{helper functions~{\nwtagstyle{}\subpageref{NWAFDpM-47YflN-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{\relax}{NWAFDpM-47YflN-2}\nwenddeflinemarkup
def free_rooms(ics_url: str) -> typing.List[typing.Tuple]:
  """
  \LA{}\code{}free{\_}rooms\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2sT7s4-1}}\RA{}
  """
  results = []
  all_rooms = set(typerconf.get(INTERESTING_ROOMS))

  \LA{}let \code{}schedule\edoc{} be the parsed ICS file we get from \code{}ics{\_}url\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-2UxnIQ-1}}\RA{}
  for event in schedule.events:
    start = event.begin.datetime
    end = event.end.datetime

    booked_rooms = event.location.split(",")
    \LA{}turn \code{}booked{\_}rooms\edoc{} into a set, properly~{\nwtagstyle{}\subpageref{NWAFDpM-3HMlH1-1}}\RA{}

    # If there are no booked rooms, we can skip this event.
    if not booked_rooms:
      continue

    unbooked_rooms = all_rooms - booked_rooms
    results.append((start, end, unbooked_rooms))

  \LA{}clean up \code{}results\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-5C1d1-1}}\RA{}
  return results
\nwalsodefined{\\{NWAFDpM-47YflN-2}}\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegincode{17}\sublabel{NWAFDpM-1n4S18-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-1}}}\moddef{imports~{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{\relax}{NWAFDpM-1n4S18-2}\nwenddeflinemarkup
import datetime
\nwalsodefined{\\{NWAFDpM-1n4S18-2}\\{NWAFDpM-1n4S18-3}}\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{18}\nwdocspar

If there are no booked rooms for a time, we get an empty string in a 
list---which will not be an empty set.
\nwenddocs{}\nwbegincode{19}\sublabel{NWAFDpM-3HMlH1-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3HMlH1-1}}}\moddef{turn \code{}booked{\_}rooms\edoc{} into a set, properly~{\nwtagstyle{}\subpageref{NWAFDpM-3HMlH1-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwenddeflinemarkup
if len(booked_rooms) == 1 and booked_rooms[0].strip() == "":
  booked_rooms = set()
else:
  booked_rooms = set(map(lambda x: x.strip(), booked_rooms))
\nwused{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwendcode{}\nwbegindocs{20}\nwdocspar

Finally, let's turn our attention to how to get the schedule containing all 
events.
Fortunately, we have the {\Tt{}nytid.schedules.read{\_}calendar\nwendquote} function.
It simply takes the URL and returns a parsed schedule.
\nwenddocs{}\nwbegincode{21}\sublabel{NWAFDpM-2UxnIQ-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-2UxnIQ-1}}}\moddef{let \code{}schedule\edoc{} be the parsed ICS file we get from \code{}ics{\_}url\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-2UxnIQ-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwenddeflinemarkup
schedule = read_calendar(ics_url)
\nwused{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwendcode{}\nwbegincode{22}\sublabel{NWAFDpM-1n4S18-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-2}}}\moddef{imports~{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-1n4S18-1}{NWAFDpM-1n4S18-3}\nwenddeflinemarkup
from nytid.schedules import read_calendar
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{23}\nwdocspar


\subsection{Clean up the results}

Now that we have the CSV, we just have to merge all the overlap.
We'll make it a simple algorithm:
If the start and end are the same, merge them by taking the union.

We'll use a dictionary to keep track of start and end times that are the same.
We use the start and end as the key, the free rooms as the value.
Then we can merge whenever the key already exists.
\nwenddocs{}\nwbegincode{24}\sublabel{NWAFDpM-5C1d1-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-5C1d1-1}}}\moddef{clean up \code{}results\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-5C1d1-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwenddeflinemarkup
time_dict = \{\}
for start, end, unbooked_rooms in results:
  if (start, end) in time_dict:
    time_dict[(start, end)] |= unbooked_rooms
  else:
    time_dict[(start, end)] = unbooked_rooms

results = [(start.strftime(DATE_FORMAT),
            end.strftime(DATE_FORMAT),
            unbooked_rooms)
           for (start, end), unbooked_rooms in time_dict.items()]
results.sort()
\nwused{\\{NWAFDpM-47YflN-1}\\{NWAFDpM-47YflN-2}}\nwendcode{}\nwbegincode{25}\sublabel{NWAFDpM-3UrjvT-3}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-3}}}\moddef{constants~{\nwtagstyle{}\subpageref{NWAFDpM-3UrjvT-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-3UrjvT-2}{\relax}\nwenddeflinemarkup
DATE_FORMAT = "%Y-%m-%d %H:%M"
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{26}\nwdocspar



\section{The \texttt{unbooked} command}

We want a command that shows the free rooms in a human readable format.
\begin{center}
{\Tt{}nytid\ utils\ rooms\ unbooked\nwendquote}
\end{center}
This function will simply call the {\Tt{}free{\_}rooms\nwendquote} function and print the CSV 
data that it returns.
\nwenddocs{}\nwbegincode{27}\sublabel{NWAFDpM-2ZPBlX-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-2ZPBlX-1}}}\moddef{\code{}unbooked\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2ZPBlX-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-2}}\nwenddeflinemarkup
Shows date and time and which rooms are free. Note that if a time is NOT in the 
list, it means that all rooms are FREE.
\nwused{\\{NWAFDpM-CyCSh-2}}\nwendcode{}\nwbegincode{28}\sublabel{NWAFDpM-CyCSh-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-2}}}\moddef{subcommands~{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-CyCSh-1}{NWAFDpM-CyCSh-3}\nwenddeflinemarkup
@cli.command(name="unbooked")
def unbooked_cmd(\LA{}arg \code{}delimiter\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-1DTQKU-1}}\RA{}):
  """
  \LA{}\code{}unbooked\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-2ZPBlX-1}}\RA{}
  """
  \LA{}let \code{}rooms{\_}url\edoc{} be the URL to the TimeEdit export~{\nwtagstyle{}\subpageref{NWAFDpM-YmNJ-1}}\RA{}
  try:
    csv_out = csv.writer(sys.stdout, delimiter=delimiter)

    for start, end, rooms in free_rooms(rooms_url):
      csv_out.writerow([start, end, ", ".join(sorted(rooms))])
  except Exception as err:
    logging.error(f"Can't get free rooms: \{err\}")
    raise typer.Exit(1)
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegincode{29}\sublabel{NWAFDpM-1n4S18-3}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-3}}}\moddef{imports~{\nwtagstyle{}\subpageref{NWAFDpM-1n4S18-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-1n4S18-2}{\relax}\nwenddeflinemarkup
import csv
import sys
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegincode{30}\sublabel{NWAFDpM-1DTQKU-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-1DTQKU-1}}}\moddef{arg \code{}delimiter\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-1DTQKU-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-2}\\{NWAFDpM-CyCSh-3}}\nwenddeflinemarkup
delimiter: delimiter_opt = "\\t",
\nwused{\\{NWAFDpM-CyCSh-2}\\{NWAFDpM-CyCSh-3}}\nwendcode{}\nwbegincode{31}\sublabel{NWAFDpM-2Ns6UP-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-2Ns6UP-1}}}\moddef{argument and option definitions~{\nwtagstyle{}\subpageref{NWAFDpM-2Ns6UP-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwenddeflinemarkup
delimiter_opt = Annotated[str,
                          typer.Option("-d", "--delimiter",
                          help="CSV delimiter, default tab.",
                          show_default=False)]
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{32}\nwdocspar


We need to get the URL to the TimeEdit export from the config.
If it's not there, we tell the user to set it.
\nwenddocs{}\nwbegincode{33}\sublabel{NWAFDpM-YmNJ-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-YmNJ-1}}}\moddef{let \code{}rooms{\_}url\edoc{} be the URL to the TimeEdit export~{\nwtagstyle{}\subpageref{NWAFDpM-YmNJ-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-2}\\{NWAFDpM-CyCSh-3}}\nwenddeflinemarkup
try:
  rooms_url = typerconf.get(BOOKED_ROOMS_URL)
except Exception as err:
  logging.error(f"Can't get URL from config: \{err\}")
  logging.info("Please set it with "
               "'nytid utils rooms set-url <url>'")
  raise typer.Exit(1)
\nwused{\\{NWAFDpM-CyCSh-2}\\{NWAFDpM-CyCSh-3}}\nwendcode{}\nwbegindocs{34}\nwdocspar


\section{The \texttt{booked} command}

We also add a command that simply prints the bookings as they are.
\begin{center}
{\Tt{}nytid\ utils\ rooms\ booked\nwendquote}
\end{center}
This is useful to see what rooms are booked and when, because we still get 
overlaps when some rooms are booked 15--17 and some 15--16.
\nwenddocs{}\nwbegincode{35}\sublabel{NWAFDpM-3BCl61-1}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-3BCl61-1}}}\moddef{\code{}booked\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-3BCl61-1}}}\endmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-CyCSh-3}}\nwenddeflinemarkup
Shows date and time and which rooms are booked.
\nwused{\\{NWAFDpM-CyCSh-3}}\nwendcode{}\nwbegincode{36}\sublabel{NWAFDpM-CyCSh-3}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-3}}}\moddef{subcommands~{\nwtagstyle{}\subpageref{NWAFDpM-CyCSh-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-CyCSh-2}{\relax}\nwenddeflinemarkup
@cli.command(name="booked")
def booked_cmd(\LA{}arg \code{}delimiter\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-1DTQKU-1}}\RA{}):
  """
  \LA{}\code{}booked\edoc{} doc~{\nwtagstyle{}\subpageref{NWAFDpM-3BCl61-1}}\RA{}
  """
  \LA{}let \code{}rooms{\_}url\edoc{} be the URL to the TimeEdit export~{\nwtagstyle{}\subpageref{NWAFDpM-YmNJ-1}}\RA{}
  try:
    csv_out = csv.writer(sys.stdout, delimiter=delimiter)

    for start, end, rooms in booked_rooms(rooms_url):
      csv_out.writerow([start, end, ", ".join(sorted(rooms))])
  except Exception as err:
    logging.error(f"Can't get booked rooms: \{err\}")
    raise typer.Exit(1)
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}\nwbegindocs{37}\nwdocspar

Now we implement the {\Tt{}booked{\_}rooms\nwendquote} function similarly to the {\Tt{}free{\_}rooms\nwendquote} 
function.
\nwenddocs{}

\nwixlogsorted{c}{{\code{}booked\edoc{} doc}{NWAFDpM-3BCl61-1}{\nwixd{NWAFDpM-3BCl61-1}\nwixu{NWAFDpM-CyCSh-3}}}%
\nwixlogsorted{c}{{\code{}free{\_}rooms\edoc{} doc}{NWAFDpM-2sT7s4-1}{\nwixd{NWAFDpM-2sT7s4-1}\nwixu{NWAFDpM-47YflN-1}}}%
\nwixlogsorted{c}{{\code{}set-url\edoc{} args}{NWAFDpM-3APbeH-1}{\nwixu{NWAFDpM-CyCSh-1}\nwixd{NWAFDpM-3APbeH-1}\nwixd{NWAFDpM-3APbeH-2}}}%
\nwixlogsorted{c}{{\code{}set-url\edoc{} doc}{NWAFDpM-2wJcSK-1}{\nwixu{NWAFDpM-CyCSh-1}\nwixd{NWAFDpM-2wJcSK-1}}}%
\nwixlogsorted{c}{{\code{}unbooked\edoc{} doc}{NWAFDpM-2ZPBlX-1}{\nwixd{NWAFDpM-2ZPBlX-1}\nwixu{NWAFDpM-CyCSh-2}}}%
\nwixlogsorted{c}{{arg \code{}delimiter\edoc{}}{NWAFDpM-1DTQKU-1}{\nwixu{NWAFDpM-CyCSh-2}\nwixd{NWAFDpM-1DTQKU-1}\nwixu{NWAFDpM-CyCSh-3}}}%
\nwixlogsorted{c}{{argument and option definitions}{NWAFDpM-2Ns6UP-1}{\nwixu{NWAFDpM-1T2u1E-1}\nwixd{NWAFDpM-2Ns6UP-1}}}%
\nwixlogsorted{c}{{clean up \code{}results\edoc{}}{NWAFDpM-5C1d1-1}{\nwixu{NWAFDpM-47YflN-1}\nwixd{NWAFDpM-5C1d1-1}\nwixu{NWAFDpM-47YflN-2}}}%
\nwixlogsorted{c}{{constants}{NWAFDpM-3UrjvT-1}{\nwixu{NWAFDpM-1T2u1E-1}\nwixd{NWAFDpM-3UrjvT-1}\nwixd{NWAFDpM-3UrjvT-2}\nwixd{NWAFDpM-3UrjvT-3}}}%
\nwixlogsorted{c}{{helper functions}{NWAFDpM-47YflN-1}{\nwixu{NWAFDpM-1T2u1E-1}\nwixd{NWAFDpM-47YflN-1}\nwixd{NWAFDpM-47YflN-2}}}%
\nwixlogsorted{c}{{imports}{NWAFDpM-1n4S18-1}{\nwixu{NWAFDpM-1T2u1E-1}\nwixd{NWAFDpM-1n4S18-1}\nwixd{NWAFDpM-1n4S18-2}\nwixd{NWAFDpM-1n4S18-3}}}%
\nwixlogsorted{c}{{let \code{}rooms{\_}url\edoc{} be the URL to the TimeEdit export}{NWAFDpM-YmNJ-1}{\nwixu{NWAFDpM-CyCSh-2}\nwixd{NWAFDpM-YmNJ-1}\nwixu{NWAFDpM-CyCSh-3}}}%
\nwixlogsorted{c}{{let \code{}schedule\edoc{} be the parsed ICS file we get from \code{}ics{\_}url\edoc{}}{NWAFDpM-2UxnIQ-1}{\nwixu{NWAFDpM-47YflN-1}\nwixd{NWAFDpM-2UxnIQ-1}\nwixu{NWAFDpM-47YflN-2}}}%
\nwixlogsorted{c}{{rooms.py}{NWAFDpM-1T2u1E-1}{\nwixd{NWAFDpM-1T2u1E-1}}}%
\nwixlogsorted{c}{{subcommands}{NWAFDpM-CyCSh-1}{\nwixu{NWAFDpM-1T2u1E-1}\nwixd{NWAFDpM-CyCSh-1}\nwixd{NWAFDpM-CyCSh-2}\nwixd{NWAFDpM-CyCSh-3}}}%
\nwixlogsorted{c}{{turn \code{}booked{\_}rooms\edoc{} into a set, properly}{NWAFDpM-3HMlH1-1}{\nwixu{NWAFDpM-47YflN-1}\nwixd{NWAFDpM-3HMlH1-1}\nwixu{NWAFDpM-47YflN-2}}}%
\nwixlogsorted{c}{{update the data in the config}{NWAFDpM-nVUiQ-1}{\nwixu{NWAFDpM-CyCSh-1}\nwixd{NWAFDpM-nVUiQ-1}\nwixd{NWAFDpM-nVUiQ-2}}}%
\nwbegincode{38}\sublabel{NWAFDpM-47YflN-2}\nwmargintag{{\nwtagstyle{}\subpageref{NWAFDpM-47YflN-2}}}\moddef{helper functions~{\nwtagstyle{}\subpageref{NWAFDpM-47YflN-1}}}\plusendmoddef\nwstartdeflinemarkup\nwusesondefline{\\{NWAFDpM-1T2u1E-1}}\nwprevnextdefs{NWAFDpM-47YflN-1}{\relax}\nwenddeflinemarkup
def booked_rooms(ics_url: str) -> typing.List[typing.Tuple]:
  """
  Given a URL or path to a TimeEdit ICS file ([[ics_url]]),
  return a ics (list of tuples) of the booked rooms:

  [(start, end, booked_rooms), ...]

  where start and end are datetime objects and booked_rooms is a set of 
  strings.
  """
  results = []

  \LA{}let \code{}schedule\edoc{} be the parsed ICS file we get from \code{}ics{\_}url\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-2UxnIQ-1}}\RA{}
  for event in schedule.events:
    start = event.begin.datetime
    end = event.end.datetime

    booked_rooms = event.location.split(",")
    \LA{}turn \code{}booked{\_}rooms\edoc{} into a set, properly~{\nwtagstyle{}\subpageref{NWAFDpM-3HMlH1-1}}\RA{}

    # If there are no booked rooms, we can skip this event.
    if not booked_rooms:
      continue

    results.append((start, end, booked_rooms))

  \LA{}clean up \code{}results\edoc{}~{\nwtagstyle{}\subpageref{NWAFDpM-5C1d1-1}}\RA{}
  return results
\nwused{\\{NWAFDpM-1T2u1E-1}}\nwendcode{}
