text: from io import BufferedReader
  from re import match, search
  from operator import itemgetter
  
  
  class Sitzungsdienst:
      """
      This class represents the weekly assignments of a single
      PDF file as published by the Staatsanwaltschaft Freiburg
      """
  
      def __init__(self, input_file: BufferedReader) -> None:
          """
          Parses & processes the given file object,
          storing the result as the `data` property
          """
  
          self.data = self.extract_data(input_file)
  
  
      def process_pages(self, pages: list) -> dict:
          """
          Processes PDF content per-page,
          returning its contents per-date
          """
  
          # Create data array
          data = {}
  
          # Initialize weekday buffer
          date = None
  
          # Extract data
          for page in pages:
              # Reset mode
              is_live = False
  
              for index, text in enumerate(page):
                  # Determine starting point ..
                  if text == 'Anfahrt':
                      is_live = True
  
                      # .. and proceed with next entry
                      continue
  
                  # Determine terminal point ..
                  if text == 'Seite':
                      is_live = False
  
                      # .. and proceed with next entry
                      continue
  
                  # Enforce entries between starting & terminal point
                  if not is_live or 'Ende der Auflistung' in text:
                      continue
  
                  # Determine current date / weekday
                  if text in ['Montag', 'Dienstag', 'Mittwoch', 'Donnerstag', 'Freitag']:
                      date = page[index + 1]
  
                      if date not in data:
                          data[date] = []
  
                      # Proceed with next entry
                      continue
  
                  # Proceed with next entry if it indicates ..
                  # (1) .. current date
                  if text == date:
                      continue
  
                  # (2) .. follow-up appointment for main trial
                  if text in ['F', '+']:
                      continue
  
                  data[date].append(text)
  
          return data
  
  
      def process_data(self, source: dict) -> list:
          """
          Processes preprocessed per-date data,
          returning data records for each court
          """
  
          # Create data array
          unprocessed = []
  
          # Iterate over source data
          for date, raw in source.items():
              buffer = []
              court  = ''
  
              # Iterate over text blocks
              for index, text in enumerate(raw):
                  if self.is_court(text):
                      court = text
  
                  else:
                      buffer.append(text)
  
                  if index == len(raw) - 1 or self.is_court(raw[index + 1]):
                      unprocessed.append({
                          'date': date,
                          'court': court,
                          'data': buffer,
                      })
  
                      # Reset buffer
                      buffer = []
  
          # Create data array
          processed = []
  
          for item in unprocessed:
              events = []
  
              # Set index of last entry
              last_index = 0
  
              for index, text in enumerate(item['data']):
                  # Upon first entry ..
                  if index == 0:
                      # .. reset index
                      last_index = 0
  
                  # Detect every ..
                  #
                  # (1) .. (Erste:r) Oberamtsanwalt / -anwältin
                  # - EOAA / EOAA'in
                  # - OAA / OAA'in
                  #
                  # (2) .. (Erste:r / Ober-) Staatsanwalt / -anwältin
                  # - OStA / OStA'in
                  # - EStA / EStA'in
                  # - StA / StA'in
                  #
                  # (3) .. Rechtsreferendar:in
                  # - Ref / Ref'in
                  if self.is_person(text):
                      # Determine current appointment
                      events.append((last_index, index + 1))
  
                      # Adjust position of last index
                      last_index = index + 1
  
              # Skip events without assignee
              if not events:
                  continue
  
              processed.append({
                  'date': item['date'],
                  'court': item['court'],
                  'events': events,
                  'infos': item['data'],
              })
  
          return processed
  
  
      def is_court(self, string: str) -> bool:
          """
          Checks whether string denotes a court
          """
  
          if match(r'(?:AG|LG)\s', string):
              return True
  
          return False
  
  
      def is_person(self, string: str) -> bool:
          """
          Checks whether string indicates a person
          """
  
          if search(r'(?:E?(?:O?StA|OAA)|Ref)(?:\'in)?', string):
              return True
  
          return False
  
  
      def is_time(self, string: str) -> bool:
          """
          Checks whether string matches a time
          """
  
          if match(r'\d{2}:\d{2}', string):
              return True
  
          return False
  
  
      def is_docket(self, string: str) -> bool:
          """
          Checks whether string matches a docket number
          """
  
          if match(r'\d{2,3}\sU?Js\s\d+/\d{2}', string):
              return True
  
          return False
  
  
      def reverse_date(self, string: str, separator: str='-') -> str:
          """
          Reverts a given date using `separator` as separator
          """
  
          return separator.join(reversed(string.split('.')))
  
  
      def format_person(self, data: list) -> str:
          """
          Converts a list of people to a human-readable string
          """
  
          # Form complete string
          string = ' '.join(data)
  
          people = []
  
          # Create people buffer
          buffer = []
  
          for text in string.split(','):
              # Remove whitespaces
              text = text.strip()
  
              # Look for title
              title = search(r'\b((?:E?(?:O?StA|OAA)|Ref)(?:\'in)?)\b', text)
  
              # Check whether text block contains title which
              # indicates last text block for current person
              if title:
                  # Iterate over buffer items
                  for index, item in enumerate(buffer):
                      # If person has PhD ..
                      if 'Dr.' in item:
                          # (1) .. remove it from current string
                          buffer[index] = item.replace('Dr.', '')
  
                          # (2) .. add PhD to buffer at proper position
                          buffer.append('Dr.')
  
                          # Abort iteration
                          break
  
                  # Add title to buffer
                  buffer.append(title[1])
  
                  # Build proper name from it
                  people.append(' '.join(reversed([item.strip() for item in buffer if item])))
  
                  # Reset buffer, but keep the rest of the string
                  buffer = [text.replace(title[1], '')]
  
              # .. otherwise ..
              else:
                  buffer.append(text)
  
          return '; '.join(people)
  
  
      def extract_data(self, pdf_file: BufferedReader) -> list:
          """
          Extracts data from PDF file, utilizing all of the
          above functions & returning the processed results
          """
  
          # Import library
          import PyPDF2
  
          # Create page data array
          pages = []
  
          # Fetch content from PDF file
          for page in PyPDF2.PdfFileReader(pdf_file).pages:
              pages.append([text.strip() for text in page.extractText().splitlines() if text])
  
          # Create data array
          data = []
  
          # Iterate over processed source data
          for item in self.process_data(self.process_pages(pages)):
              # Create data buffer
              details = []
  
              # Create buffer for place & assignee(s)
              where = []
              who   = []
  
              # Iterate over indices of each event
              for event in item['events']:
                  # Create buffer for time & docket number
                  when  = ''
                  what  = ''
  
                  for index in range(event[0], event[1]):
                      entry = item['infos']
  
                      # Parse strings, which are either ..
                      # (1) .. time
                      if self.is_time(entry[index]):
                          # Apply findings
                          when = entry[index]
  
                      # (2) .. docket number
                      elif self.is_docket(entry[index]):
                          # Apply findings
                          what = entry[index]
  
                      # (3) .. person
                      elif self.is_person(entry[index]):
                          # If entry before this one is no docket ..
                          if not self.is_docket(entry[index - 1]):
                              # .. add it
                              who.append(entry[index - 1])
  
                          # Add current entry
                          who.append(entry[index])
  
                      # (4) .. something else
                      else:
                          # If next entry is not a person ..
                          if not self.is_person(entry[index + 1]):
                              # .. treat current entry as
                              where.append(entry[index])
  
                  # If time & docket number are specified ..
                  if when + what:
                      # (1) .. add them to the buffer
                      details.append((when, what, who))
  
                      # (2) .. reset assignee(s)
                      who = []
  
                  # .. otherwise instead of creating an empty entry ..
                  else:
                      # .. add assignee to last entry
                      details[-1] = list(details[-1])[:-1] + [who]
  
              # Iterate over result in order to ..
              for detail in details:
                  # .. combine & store them
                  data.append({
                      'date': self.reverse_date(item['date']),
                      'when': detail[0],
                      'who': self.format_person(detail[2]),
                      'where': ' '.join([item['court'].replace(' ,', '')] + where),
                      'what': detail[1],
                  })
  
          return sorted(data, key=itemgetter('date', 'who', 'when', 'where', 'what'))
  
  
      def filter(self, query: list) -> list:
          """
          Filters the currently stored data by each
          `query`term, returning the search results
          """
  
          # Create data buffer
          buffer = []
  
          # Loop over search terms in order to ..
          for term in query:
              # .. filter out relevant items
              buffer += [item for item in self.data if term.lower() in item['who'].lower()]
  
          # Apply data buffer
          return buffer
  
  
      def date_range(self) -> tuple:
          """Determines date range for the currently stored data"""
  
          return (self.data[0]['date'], self.data[-1]['date'])
  