def test_isPlaceholder_personWithExternalId(self): sentence_group = StateSentenceGroup.new_with_defaults( state_code=_STATE_CODE) person = StatePerson.new_with_defaults( sentence_groups=[sentence_group]) self.assertTrue(is_placeholder(person)) person.external_ids.append( StatePersonExternalId.new_with_defaults(state_code=_STATE_CODE, external_id=_EXTERNAL_ID, id_type=_ID_TYPE)) self.assertFalse(is_placeholder(person))
def test_isPlaceholder_personWithExternalId(self) -> None: sentence_group = schema.StateSentenceGroup( state_code=_STATE_CODE, status=StateSentenceStatus.PRESENT_WITHOUT_INFO) person = schema.StatePerson(state_code=_STATE_CODE, sentence_groups=[sentence_group]) self.assertTrue(is_placeholder(person)) person.external_ids.append( schema.StatePersonExternalId(state_code=_STATE_CODE, external_id=_EXTERNAL_ID, id_type=_ID_TYPE)) self.assertFalse(is_placeholder(person))
def _move_periods_onto_sentences_for_sentence_group( sentence_group: schema.StateSentenceGroup, period_type: Type[schema.SchemaPeriodType]): """Looks at all SupervisionPeriods in the provided |sentence_group|, and attempts to match them to any corresponding sentences, based on date. """ sentences = sentence_group.supervision_sentences + sentence_group.incarceration_sentences # Get all periods from sentence group periods = get_all_entities_of_cls([sentence_group], period_type) # Clear non-placeholder links from sentence to period. We will re-add/update these relationships below. for sentence in sentences: _only_keep_placeholder_periods_on_sentence(sentence, period_type) unmatched_periods = [] matchable_sentences = _get_date_matchable_sentences(sentences) non_placeholder_periods = [p for p in periods if not is_placeholder(p)] # Match periods to non_placeholder_sentences by date. for p in non_placeholder_periods: matched = False p_start_date = _get_period_start_date(p) p_end_date = _get_period_end_date(p) for s in matchable_sentences: s_start_date = s.start_date if not s_start_date: continue s_completion_date = s.completion_date if s.completion_date else datetime.date.max if date_spans_overlap_exclusive( start_1=p_start_date, end_1=p_end_date, start_2=s_start_date, end_2=s_completion_date): matched = True _add_period_to_sentence(p, s) # Unmatched periods will be re-added to a placeholder sentence at the end. if not matched: unmatched_periods.append(p) # Add unmatched periods to a placeholder sentence if unmatched_periods: placeholder_sentences = [s for s in sentences if is_placeholder(s)] if not placeholder_sentences: placeholder_sentence = get_or_create_placeholder_child( sentence_group, 'supervision_sentences', schema.StateSupervisionSentence, state_code=sentence_group.state_code, status=StateSentenceStatus.PRESENT_WITHOUT_INFO.value, person=sentence_group.person) else: placeholder_sentence = placeholder_sentences[0] for unmatched_period in unmatched_periods: _add_period_to_sentence(unmatched_period, placeholder_sentence)
def _is_match(*, ingested_entity: Entity, db_entity: Entity) -> bool: """Returns true if the provided |ingested_entity| matches the provided |db_entity|. Otherwise returns False. """ if not ingested_entity or not db_entity: return ingested_entity == db_entity if ingested_entity.__class__ != db_entity.__class__: raise EntityMatchingError( f"is_match received entities of two different classes: " f"ingested entity {ingested_entity.__class__.__name__} and " f"db_entity {db_entity.__class__.__name__}", ingested_entity.get_entity_name()) if isinstance(ingested_entity, StatePerson): db_entity = cast(StatePerson, db_entity) for ingested_external_id in ingested_entity.external_ids: for db_external_id in db_entity.external_ids: if _is_match(ingested_entity=ingested_external_id, db_entity=db_external_id): return True return False if isinstance(ingested_entity, StatePersonExternalId): db_entity = cast(StatePersonExternalId, db_entity) return ingested_entity.state_code == db_entity.state_code \ and ingested_entity.external_id == db_entity.external_id \ and ingested_entity.id_type == db_entity.id_type # As person has already been matched, assume that any of these 'person # attribute' entities are matches if their state_codes align. if isinstance(ingested_entity, StatePersonAlias): db_entity = cast(StatePersonAlias, db_entity) return ingested_entity.state_code == db_entity.state_code \ and ingested_entity.full_name == db_entity.full_name if isinstance(ingested_entity, StatePersonRace): db_entity = cast(StatePersonRace, db_entity) return ingested_entity.state_code == db_entity.state_code \ and ingested_entity.race == db_entity.race if isinstance(ingested_entity, StatePersonEthnicity): db_entity = cast(StatePersonEthnicity, db_entity) return ingested_entity.state_code == db_entity.state_code \ and ingested_entity.ethnicity == db_entity.ethnicity db_entity = cast(ExternalIdEntity, db_entity) ingested_entity = cast(ExternalIdEntity, ingested_entity) # Placeholders entities are considered equal if ingested_entity.external_id is None and db_entity.external_id is None: return is_placeholder(ingested_entity) and is_placeholder(db_entity) return ingested_entity.external_id == db_entity.external_id
def _match_entity_tree(*, ingested_entity_tree: EntityTree, db_entity_trees: List[EntityTree], matched_entities_by_db_ids: Dict[int, Entity], root_entity_cls: Type) -> IndividualMatchResult: """Attempts to match the provided |ingested_entity_tree| to one of the provided |db_entity_trees|. If a successful match is found, merges the ingested entity onto the matching database entity and performs entity matching on all children of the matched entities. Returns the results of matching as an IndividualMatchResult. """ if is_placeholder(ingested_entity_tree.entity): return _match_placeholder_tree( ingested_placeholder_tree=ingested_entity_tree, db_entity_trees=db_entity_trees, matched_entities_by_db_ids=matched_entities_by_db_ids, root_entity_cls=root_entity_cls) db_match_tree = _get_match(ingested_entity_tree, db_entity_trees) if not db_match_tree: return _match_unmatched_tree( ingested_unmatched_entity_tree=ingested_entity_tree, db_entity_trees=db_entity_trees, root_entity_cls=root_entity_cls) return _match_matched_tree( ingested_entity_tree=ingested_entity_tree, db_match_tree=db_match_tree, matched_entities_by_db_ids=matched_entities_by_db_ids, root_entity_cls=root_entity_cls)
def _find_violation_events( self, violations: List[StateSupervisionViolation], ) -> List[ViolationEvent]: """Finds instances of a violation. Identifies instance of a violation occurring. Args: - violations: All of the person's StateSupervisionViolations - violation_responses: All of the person's StateSupervisionViolationResponses Returns: A list of ViolationEvents for the person. """ violation_events: List[ViolationEvent] = [] for violation in violations: if is_placeholder(violation): continue violation_with_response_events = self._find_violation_with_response_events( violation) violation_events.extend(violation_with_response_events) return violation_events
def validate_admission_data( incarceration_periods: List[StateIncarcerationPeriod]) -> \ List[StateIncarcerationPeriod]: """Removes any incarceration periods that don't have admission dates or admission reasons. Returns the valid incarceration periods. """ validated_incarceration_periods: List[StateIncarcerationPeriod] = [] for incarceration_period in incarceration_periods: if is_placeholder(incarceration_period): # Drop any placeholder incarceration periods from the calculations continue if not incarceration_period.admission_date: logging.info( "No admission_date on incarceration period with" " id: %d", incarceration_period.incarceration_period_id) continue if not incarceration_period.admission_reason: logging.info( "No admission_reason on incarceration period with" " id: %d", incarceration_period.incarceration_period_id) continue validated_incarceration_periods.append(incarceration_period) return validated_incarceration_periods
def validate_admission_data( incarceration_periods: List[StateIncarcerationPeriod]) -> \ List[StateIncarcerationPeriod]: """Removes any incarceration periods that don't have admission dates or admission reasons. Returns the valid incarceration periods. """ if incarceration_periods and incarceration_periods[0].state_code == 'US_ND': # If these are North Dakota incarceration periods, send to the # state-specific ND data validation function incarceration_periods = us_nd_utils.set_missing_admission_data( incarceration_periods) validated_incarceration_periods: List[StateIncarcerationPeriod] = [] for incarceration_period in incarceration_periods: if is_placeholder(incarceration_period): # Drop any placeholder incarceration periods from the calculations continue if not incarceration_period.admission_date: logging.info( "No admission_date on incarceration period with" " id: %d", incarceration_period.incarceration_period_id) continue if not incarceration_period.admission_reason: logging.info( "No admission_reason on incarceration period with" " id: %d", incarceration_period.incarceration_period_id) continue validated_incarceration_periods.append(incarceration_period) return validated_incarceration_periods
def _drop_placeholder_periods(supervision_periods: List[StateSupervisionPeriod]) -> \ List[StateSupervisionPeriod]: """Drops all supervision periods where the custodial_authority is not the state DOC of the state_code on the supervision_period.""" return [ period for period in supervision_periods if not is_placeholder(period) ]
def _base_entity_match( a: DatabaseEntity, b: DatabaseEntity, skip_fields: Set[str], allow_null_mismatch: bool = False ) -> bool: """Returns whether two objects of the same type are an entity match. Args: a: The first entity to match b: The second entity to match skip_fields: A list of names of fields that should be ignored when determining if two objects match based on flat fields. allow_null_mismatch: Allow for two objects to still match if one has a null value in a field where the other's is nonnull. """ # Placeholders never match if is_placeholder(a) or is_placeholder(b): return False # Compare external ids if one is present if a.get_external_id() or b.get_external_id(): return a.get_external_id() == b.get_external_id() # Compare all flat fields of the two entities all_set_flat_field_names = \ get_set_entity_field_names(a, EntityFieldType.FLAT_FIELD) | \ get_set_entity_field_names(b, EntityFieldType.FLAT_FIELD) for field_name in all_set_flat_field_names: # Skip primary key if field_name == a.get_class_id_name() or field_name in skip_fields: continue a_field = a.get_field(field_name) b_field = b.get_field(field_name) if allow_null_mismatch and (a_field is None or b_field is None): # Do not disqualify a match if one of the fields is null continue if a_field != b_field: return False return True
def _get_root_entity_helper(entity: Entity) -> Optional[Type]: if not is_placeholder(entity): return entity.__class__ for field_name in get_set_entity_field_names(entity, EntityFieldType.FORWARD_EDGE): field = get_field_as_list(entity, field_name)[0] result = _get_root_entity_helper(field) if result is not None: return result return None
def _add_match_to_matched_entities_cache( *, db_entity_match: Entity, ingested_entity: Entity, matched_entities_by_db_ids: Dict[int, Entity]): """Records a new ingested_entity/db_entity match. If the DB entity has already been matched to a different ingested_entity, it raises an error. """ matched_db_id = db_entity_match.get_id() if matched_db_id in matched_entities_by_db_ids: if ingested_entity != matched_entities_by_db_ids[matched_db_id]: matches = [ ingested_entity, matched_entities_by_db_ids[matched_db_id] ] # It's ok for a DB object to match multiple ingested placeholders. if is_placeholder(matches[0]) and is_placeholder(matches[1]): return raise MatchedMultipleIngestedEntitiesError(db_entity_match, matches) else: matched_entities_by_db_ids[matched_db_id] = ingested_entity
def find_supervision_periods_overlapping_with_date( event_date: date, supervision_periods: List[StateSupervisionPeriod], ) -> List[StateSupervisionPeriod]: """Identifies supervision_periods where the event_date falls between the start and end of the supervision period, inclusive of the start date and exclusive of the end date.""" return [ sp for sp in supervision_periods if not is_placeholder(sp) and sp.start_date is not None and sp.start_date <= event_date and ( sp.termination_date is None or event_date < sp.termination_date) ]
def _merge_incarceration_periods_helper( incomplete_incarceration_periods: List[StateIncarcerationPeriod]) \ -> List[StateIncarcerationPeriod]: """Using the provided |incomplete_incarceration_periods|, attempts to merge consecutive admission and release periods from the same facility. Returns a list containing all merged incarceration periods as well as all incarceration periods that could not be merged, all ordered chronologically (based on the movement sequence number provided directly from ND). """ placeholder_periods = [ p for p in incomplete_incarceration_periods if is_placeholder(p) ] non_placeholder_periods = [ p for p in incomplete_incarceration_periods if not is_placeholder(p) ] # Within any IncarcerationSentence, IncarcerationPeriod external_ids are all # equivalent, except for their suffixes. Each suffix is based on the # ND-provided movement sequence number. Therefore, sorting by external_ids # is equivalent to sorting by this movement sequence number. sorted_periods = sorted(non_placeholder_periods, key=_get_sequence_no) merged_periods = [] last_period = None for period in sorted_periods: if not last_period: last_period = period continue if is_incomplete_incarceration_period_match(last_period, period): merged_periods.append( _merge_incomplete_periods(period, last_period)) last_period = None else: merged_periods.append(last_period) last_period = period if last_period: merged_periods.append(last_period) merged_periods.extend(placeholder_periods) return merged_periods
def _only_keep_placeholder_periods_on_sentence( sentence: schema.SchemaSentenceType, period_type: Type[schema.SchemaPeriodType]): """Removes all non placeholder periods of type |period_type| from the provided |sentence|.""" sentence_periods = sentence.supervision_periods \ if period_type == schema.StateSupervisionPeriod \ else sentence.incarceration_periods placeholder_periods = [p for p in sentence_periods if is_placeholder(p)] if period_type == schema.StateSupervisionPeriod: sentence.supervision_periods = placeholder_periods else: sentence.incarceration_periods = placeholder_periods
def test_set_missing_admission_data_placeholders(self): """Tests that the function removes placeholder incarceration periods.""" placeholder = StateIncarcerationPeriod.new_with_defaults( state_code='US_ND') incarceration_periods = [placeholder] assert is_placeholder(placeholder) updated_incarceration_periods = \ set_missing_admission_data(incarceration_periods) assert updated_incarceration_periods == []
def is_incomplete_incarceration_period_match( ingested_entity: Union[EntityTree, Entity], db_entity: Union[EntityTree, Entity]) -> bool: """Given two incomplete StateIncarcerationPeriods, determines if they should be considered the same StateIncarcerationPeriod. """ a, b = ingested_entity, db_entity if isinstance(ingested_entity, EntityTree): db_entity = cast(EntityTree, db_entity) a, b = ingested_entity.entity, db_entity.entity a = cast(StateIncarcerationPeriod, a) b = cast(StateIncarcerationPeriod, b) # Cannot match with a placeholder StateIncarcerationPeriod if is_placeholder(a) or is_placeholder(b): return False a_seq_no = _get_sequence_no(a) b_seq_no = _get_sequence_no(b) # Only match incomplete periods if they are adjacent based on seq no. if abs(a_seq_no - b_seq_no) != 1: return False # Check that the first period is an admission and second a release if a_seq_no < b_seq_no: first, second = a, b else: first, second = b, a if not first.admission_date or not second.release_date: return False # Must have same facility if a.facility != b.facility: return False return True
def _base_entity_match(a: DatabaseEntity, b: DatabaseEntity) -> bool: # Placeholders never match if is_placeholder(a) or is_placeholder(b): return False # Compare external ids if one is present if a.get_external_id() or b.get_external_id(): return a.get_external_id() == b.get_external_id() # Compare all flat fields of the two entities all_set_flat_field_names = \ get_set_entity_field_names(a, EntityFieldType.FLAT_FIELD) | \ get_set_entity_field_names(b, EntityFieldType.FLAT_FIELD) for field_name in all_set_flat_field_names: # Skip primary key if field_name == a.get_class_id_name(): continue a_field = a.get_field(field_name) b_field = b.get_field(field_name) if a_field != b_field: return False return True
def _filter_sentences_with_missing_fields( sentences: List[SentenceType]) -> List[SentenceType]: valid_sentences: List[SentenceType] = [] for sentence in sentences: if is_placeholder(sentence): continue if not sentence.start_date: logging.error( "Non-placeholder sentence [%s] for state [%s] has no start date - ignoring.", sentence.external_id, sentence.state_code) continue valid_sentences.append(sentence) return valid_sentences
def get_or_create_placeholder_child( parent_entity: DatabaseEntity, child_field_name: str, child_class: Type[DatabaseEntity], **child_kwargs): """Checks all the entities in the |parent_entity|'s field |child_field_name|. If there is a placeholder entity, returns that. Otherwise creates a new placeholder entity of type |child_class| on the parent's |child_field_name| using |child_kw_args|. """ children = parent_entity.get_field_as_list(child_field_name) placeholder_children = [c for c in children if is_placeholder(c)] if placeholder_children: return placeholder_children[0] logging.info( 'No placeholder children on entity with id [%s] of type [%s] exist on field [%s]. Have to create one.', parent_entity.get_external_id(), parent_entity.get_entity_name(), child_field_name) new_child = child_class(**child_kwargs) if not is_placeholder(new_child): raise EntityMatchingError( f'Child created with kwargs is not a placeholder [{child_kwargs}]. Parent entity: [{parent_entity}]. Child ' f'field name: [{child_field_name}]. Child class: [{child_class}].', parent_entity.get_entity_name()) children.append(new_child) parent_entity.set_field_from_list(child_field_name, children) return new_child
def _match_persons( *, ingested_persons: List[StatePerson], db_persons: List[StatePerson]) \ -> MatchedEntities: """Attempts to match all persons from |ingested_persons| with the provided |db_persons|. Results are returned in the MatchedEntities object which contains all successfully matched and merged persons as well as an error count that is incremented every time an error is raised matching an ingested person. """ db_person_trees = [ EntityTree(entity=db_person, ancestor_chain=[]) for db_person in db_persons ] ingested_person_trees = [ EntityTree(entity=ingested_person, ancestor_chain=[]) for ingested_person in ingested_persons ] root_entity_cls = get_root_entity_cls(ingested_persons) total_root_entities = get_total_entities_of_cls(ingested_persons, root_entity_cls) persons_match_results = _match_entity_trees( ingested_entity_trees=ingested_person_trees, db_entity_trees=db_person_trees, root_entity_cls=root_entity_cls) updated_persons = [] for match_result in persons_match_results.individual_match_results: if not match_result.merged_entity_trees: updated_persons.append(match_result.ingested_entity_tree.entity) else: # It is possible that multiple ingested people match to the same # DB person, in which case we should only keep one reference to # that object. for merged_person_tree in match_result.merged_entity_trees: if merged_person_tree.entity not in updated_persons: updated_persons.append(merged_person_tree.entity) # The only database persons that are unmatched that we potentially want to # update are placeholder persons. These may have had children removed as # a part of the matching process and therefore would need updating. for db_person in persons_match_results.unmatched_db_entities: if is_placeholder(db_person): updated_persons.append(db_person) return MatchedEntities(people=updated_persons, error_count=persons_match_results.error_count, total_root_entities=total_root_entities)
def add_supervising_officer_to_open_supervision_periods(persons: List[schema.StatePerson]): """For each person in the provided |persons|, adds the supervising_officer from the person entity onto all open StateSupervisionPeriods. """ for person in persons: if not person.supervising_officer: continue supervision_periods = get_all_entities_of_cls([person], schema.StateSupervisionPeriod) for supervision_period in supervision_periods: # Skip placeholders if is_placeholder(supervision_period): continue if not supervision_period.termination_date: supervision_period.supervising_officer = person.supervising_officer
def _get_date_matchable_sentences(sentences: List[schema.SchemaSentenceType]) -> List[schema.SchemaSentenceType]: """Filters the provided list of |sentences| to only include sentences which are able to be matched to periods based on date. Returns this filtered list. """ valid_sentences = [] for sentence in sentences: if is_placeholder(sentence): continue # If we have sentences which we know are complete, but we have no completion date, exclude them from date # matching. if _is_sentence_ended_by_status(sentence) and not sentence.completion_date: continue valid_sentences.append(sentence) return valid_sentences
def _get_periods_in_sentence_group(sentence_group: schema.StateSentenceGroup) \ -> Tuple[List[schema.StateIncarcerationPeriod], List[schema.StateIncarcerationPeriod]]: """Finds all placeholder and non-placeholder StateIncarcerationPeriods in the provided |sentence_group|, and returns the two lists in a tuple.""" placeholder_periods = [] non_placeholder_periods = [] for incarceration_sentence in \ sentence_group.incarceration_sentences: for incarceration_period in \ incarceration_sentence.incarceration_periods: if is_placeholder(incarceration_period): placeholder_periods.append(incarceration_period) else: non_placeholder_periods.append(incarceration_period) return placeholder_periods, non_placeholder_periods
def find_supervision_periods_during_referral( referral_date: date, supervision_periods: List[StateSupervisionPeriod], ) -> List[StateSupervisionPeriod]: """Identifies supervision_periods where the referral_date falls between the start and end of the supervision period, indicating that the person was serving this supervision period at the time of the referral.""" # Get all valid supervision periods with a start date before or on the # referral date applicable_supervision_periods = [ sp for sp in supervision_periods if not is_placeholder(sp) and sp.start_date is not None and sp.start_date <= referral_date and (sp.termination_date is None or sp.termination_date >= referral_date) ] return applicable_supervision_periods
def set_missing_admission_data( incarceration_periods: List[StateIncarcerationPeriod]) -> \ List[StateIncarcerationPeriod]: """Sets missing admission data on incarceration period inputs according to logic specific to North Dakota data. In the ND data, there are over 1,000 incarceration periods with empty admission dates and reasons that follow a release for a transfer. These incarceration periods have valid release data, so we know that this individual was transferred back into a facility at some point. For every incarceration period where this is the case, this function sets the admission date to the date of the transfer out, and the admission reason to be a transfer back in. """ filtered_incarceration_periods: List[StateIncarcerationPeriod] = [] # Remove placeholder incarceration periods and any without an external_id for incarceration_period in incarceration_periods: if not is_placeholder(incarceration_period) and \ incarceration_period.external_id is not None: filtered_incarceration_periods.append(incarceration_period) updated_incarceration_periods: List[StateIncarcerationPeriod] = [] if filtered_incarceration_periods: filtered_incarceration_periods.sort(key=lambda b: b.external_id) for index, incarceration_period in \ enumerate(filtered_incarceration_periods): if not incarceration_period.admission_date and \ not incarceration_period.admission_reason and index > 0: previous_incarceration_period = \ filtered_incarceration_periods[index - 1] if previous_incarceration_period.release_reason == \ StateIncarcerationPeriodReleaseReason.TRANSFER and \ previous_incarceration_period.release_date \ is not None: incarceration_period.admission_reason = \ StateIncarcerationPeriodAdmissionReason.TRANSFER incarceration_period.admission_date = \ previous_incarceration_period.release_date updated_incarceration_periods.append(incarceration_period) return updated_incarceration_periods
def set_current_supervising_officer_from_supervision_periods( matched_persons: List[schema.StatePerson]): """For every matched person, update the supervising_officer field to pull in the supervising_officer from the latest supervision period (sorted by termination date). """ for person in matched_persons: sps = get_all_entities_of_cls(person.sentence_groups, schema.StateSupervisionPeriod) non_placeholder_sps = [sp for sp in sps if not is_placeholder(sp)] if not non_placeholder_sps: continue non_placeholder_sps.sort(key=lambda sp: sp.termination_date if sp.termination_date else datetime.date.max) latest_supervision_period = non_placeholder_sps[-1] person.supervising_officer = latest_supervision_period.supervising_officer
def _move_events_onto_supervision_periods( source: DatabaseEntity, event_cls: Type[DatabaseEntity], event_field_name: str ) -> List[DatabaseEntity]: """Looks at all events of type |event_cls| in the provided |source|, and attempts to place them onto a matching SupervisionPeriod's |event_field_name| field. Matching is based on dates, and all unmatched events are returned to the caller to store. """ supervision_periods = get_all_entities_of_cls( [source], schema.StateSupervisionPeriod ) events = get_all_entities_of_cls(supervision_periods, event_cls) # Clear the links from supervision period to supervision violations. We will # re-add/update these relationships below. for supervision_period in supervision_periods: supervision_period.set_field(event_field_name, []) unmatched_events = [] non_placeholder_periods = [ sp for sp in supervision_periods if not is_placeholder(sp) ] # Match events onto to non_placeholder_periods by date. for event in events: matched = False event_date = _get_event_date(event) if event_date: for sp in non_placeholder_periods: sp_end_date = ( sp.termination_date if sp.termination_date else datetime.date.max ) sp_start_date = sp.start_date if sp_start_date <= event_date < sp_end_date: matched = True sp_events = sp.get_field(event_field_name) sp_events.append(event) sp.set_field(event_field_name, sp_events) # Unmatched SVs will be returned if not matched: unmatched_events.append(event) return unmatched_events
def test_set_missing_admission_data_no_external_id(self): """Tests that the function removes incarceration periods without external_ids.""" incarceration_period = StateIncarcerationPeriod.new_with_defaults( state_code='US_ND', incarceration_period_id=2222, status=StateIncarcerationPeriodStatus.NOT_IN_CUSTODY, admission_date=date(2008, 4, 14), admission_reason=AdmissionReason.TRANSFER, release_date=date(2010, 4, 14), release_reason=ReleaseReason.SENTENCE_SERVED) incarceration_periods = [incarceration_period] assert not is_placeholder(incarceration_period) assert incarceration_period.external_id is None updated_incarceration_periods = \ set_missing_admission_data(incarceration_periods) assert updated_incarceration_periods == []
def get_month_supervision_type( any_date_in_month: datetime.date, supervision_sentences: List[StateSupervisionSentence], incarceration_sentences: List[StateIncarcerationSentence], supervision_period: StateSupervisionPeriod ) -> StateSupervisionPeriodSupervisionType: """Supervision type can change over time even if the period does not change. This function calculates the supervision type that a given supervision period represents during the month that |any_date_in_month| falls in. We do this by looking at all sentences attached to this supervision period, then determining which ones overlap with any day in the month, and using the sentence supervision types to determine the period supervision type at this point in time. Args: any_date_in_month: (date) Any day in the month to consider supervision_period: (StateSupervisionPeriod) The supervision period we want to associate a supervision type with supervision_sentences: (List[StateSupervisionSentence]) All supervision sentences for a given person. """ if not supervision_period.supervision_period_id: raise ValueError('All objects should have database ids.') if is_placeholder(supervision_period): raise ValueError('Do not expect placeholder periods!') start_of_month = first_day_of_month(any_date_in_month) end_of_month = last_day_of_month(any_date_in_month) # Find sentences that are attached to the period and overlap with the month incarceration_sentences = _get_valid_attached_sentences( incarceration_sentences, supervision_period) incarceration_sentences = _get_sentences_overlapping_with_dates( start_of_month, end_of_month, incarceration_sentences) supervision_sentences = _get_valid_attached_sentences( supervision_sentences, supervision_period) supervision_sentences = _get_sentences_overlapping_with_dates( start_of_month, end_of_month, supervision_sentences) return _get_supervision_type_from_sentences(incarceration_sentences, supervision_sentences)