docs: def extract_subjects(subject_info_xml, primary_str):
      """Extract a set of authenticated subjects from a DataONE SubjectInfo.
  
      - See subject_info_tree for details.
  
      Args:
          subject_info_xml : str
              A SubjectInfo XML document.
  
          primary_str : str
              A DataONE subject, typically a DataONE compliant serialization of the DN of
              the DataONE X.509 v3 certificate extension from which the SubjectInfo was
              extracted.
  
              The primary subject can be viewed as the root of a tree. Any subject in the
              SubjectInfo that is directly or indirectly connected to the root subject is
              included in the returned set of authenticated subjects.
  
  
      Returns:
          set: Set of authenticated subjects. Will always include the primary subject.
  
          - All subjects in the returned set are equivalent to ``primary_str`` for the
            purpose of access control for private science objects.
  
          - If SubjectInfo does not contain all relevant records, it is still considered
            to be valid, but the authenticated set will be incomplete.
  
          - Only the subject strings and relationships in SubjectInfo are used by this
            function. Other information about subjects, such as name and email address,
            is ignored.
  
          - No attempt should be made to infer type of subject from the content of a
            subject string. Subject strings should be handled as random Unicode
            sequences, each of which may designate an person subject, an equivalent
            subject, or a group subject.
  
          - To determine if an action is authorized, the returned set is checked against
            the authorized_set for a given object. If one or more subjects exist in both
            sets, the action is authorized. The check can be performed with high
            performance using a set union operation in Python or an inner join in
            Postgres.
  
          - Subject types are only known and relevant while processing the SubjectInfo
            type.
          - The type of each subject in the authenticated_subjects and allowed_subjects
            lists are unknown and irrelevant.
  
      Notes:
          Procedure:
  
          The set of authenticated subjects is generated from the SubjectInfo and primary
          subject using the following procedure:
  
          - Start with empty set of subjects
          - Add authenticatedUser
          - If ``subject`` is not in set of subjects:
          - Add ``subject``
          - Iterate over Person records
          - If Person.subject is ``subject``:
          - If Person.verified is present and set:
          - Add "verifiedUser"
          - Iterate over Person.equivalentIdentity:
          - Recursively add those subjects
          - Iterate over Person.isMemberOf
          - Recursively add those subjects, but ONLY check Group subjects
          - Iterate over Group records
          - If any Group.hasMember is ``subject``:
          - Recursively add Group.subject (not group members)
  
  
          Handling of various invalid SubjectInfo and corner cases:
  
          - SubjectInfo XML doc that is not well formed
          - Return an exception that includes a useful error message with the line number
            of the issue
  
          - person.isMemberOf and group.hasMember should always form pairs referencing
            each other.
          - One side of the pair is missing
          - Process the available side as normal
          - person.isMemberOf subject references a person or equivalent instead of a
            group
          - Only Group subjects are searched for isMemberOf references, so only the
            referenced Group subject is added to the list of authorized subjects
  
          - Multiple Person or Group records conflict by using the same subject
          - The records are handled as equivalents
  
          - person.isMemberOf subject does not reference a known subject
          - If the Person containing the dangling isMemberOf IS NOT connected with the
            authenticated subject, the whole record, including the isMemberOf subject is
            simply ignored
          - If it IS connected with an authenticated subject, the isMemberOf subject is
            authenticated and recursive processing of the subject is skipped
  
          - Circular references
          - Handled by skipping recursive add for subjects that are already added
  
          - See the unit tests for example SubjectInfo XML documents for each of these
            issues and the expected results.
  
      """
      subject_info_pyxb = deserialize_subject_info(subject_info_xml)
      subject_info_tree = gen_subject_info_tree(subject_info_pyxb, primary_str)
      return subject_info_tree.get_subject_set()