def supervision_period_supervision_type_mapper( label: str, ) -> Optional[StateSupervisionPeriodSupervisionType]: """Parses status information from the 'offstat' table into potential supervision types. Ranking of priority is taken from Idaho itself (the 'statstrt' table in the us_id_raw_data dataset). """ statuses = sorted_list_from_str(label, delimiter=" ") if "PR" in statuses and "PB" in statuses: # Parole and Probation return StateSupervisionPeriodSupervisionType.DUAL if "PR" in statuses: # Parole return StateSupervisionPeriodSupervisionType.PAROLE if "PB" in statuses: # Probation return StateSupervisionPeriodSupervisionType.PROBATION if "PS" in statuses: # Pre sentence investigation return StateSupervisionPeriodSupervisionType.INVESTIGATION if ( "PA" in statuses ): # Pardon applicant TODO(#3506): Get more info from ID. Filter these people out entirely? return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if ( "PF" in statuses ): # Firearm applicant TODO(#3506): Get more info from ID. Filter these people out entirely? return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if "CR" in statuses: # Rider (no longer used). return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if "BW" in statuses: # Bench Warrant return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if "CP" in statuses: # Court Probation return StateSupervisionPeriodSupervisionType.INFORMAL_PROBATION if "TM" in statuses: # Termer # Note: This in general shouldn't be showing up since Termer is an # incarceration type and not a supervision type. We have this as # a fallback here to handle erroneous instances that we've seen. return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN return None
def supervision_period_supervision_type_mapper( label: str) -> Optional[StateSupervisionPeriodSupervisionType]: """Parses status information from the 'offstat' table into potential supervision types. Ranking of priority is taken from Idaho itself (the 'statstrt' table in the us_id_raw_data dataset). """ statuses = sorted_list_from_str(label, delimiter=' ') if 'PR' in statuses and 'PB' in statuses: # Parole and Probation return StateSupervisionPeriodSupervisionType.DUAL if 'PR' in statuses: # Parole return StateSupervisionPeriodSupervisionType.PAROLE if 'PB' in statuses: # Probation return StateSupervisionPeriodSupervisionType.PROBATION if 'PS' in statuses: # Pre sentence investigation return StateSupervisionPeriodSupervisionType.INVESTIGATION if 'PA' in statuses: # Pardon applicant TODO(#3506): Get more info from ID. Filter these people out entirely? return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if 'PF' in statuses: # Firearm applicant TODO(#3506): Get more info from ID. Filter these people out entirely? return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if 'CR' in statuses: # Rider (no longer used). return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if 'BW' in statuses: # Bench Warrant return StateSupervisionPeriodSupervisionType.INTERNAL_UNKNOWN if 'CP' in statuses: # Court Probation return StateSupervisionPeriodSupervisionType.INFORMAL_PROBATION return None
def purpose_for_incarceration_mapper( label: str, ) -> Optional[StateSpecializedPurposeForIncarceration]: """Parses status information from the 'offstat' table into potential purposes for incarceration. Ranking of priority is taken from Idaho itself (the 'statstrt' table in the us_id_raw_data dataset). """ statuses = sorted_list_from_str(label, delimiter=" ") if "TM" in statuses: # Termer return StateSpecializedPurposeForIncarceration.GENERAL if "RJ" in statuses: # Rider return StateSpecializedPurposeForIncarceration.TREATMENT_IN_PRISON if ( "NO" in statuses ): # Non Idaho commitment TODO(#3518): Consider adding as specialized purpose. return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if "PV" in statuses: # Parole board hold return StateSpecializedPurposeForIncarceration.PAROLE_BOARD_HOLD if ( "IP" in statuses ): # Institutional Probation TODO(#3518): Understand what this is. return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if ( "CV" in statuses ): # Civil commitment TODO(#3518): Consider adding a specialized purpose for this return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if "CH" in statuses: # Courtesy Hold TODO(#3518): Understand what this is return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if ( "PB" in statuses ): # Probation -- happens VERY infrequently (occurs as a data error from ID) return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN return None
def supervision_period_admission_reason_mapper( label: str, ) -> Optional[StateSupervisionPeriodAdmissionReason]: """Maps |label|, a space delimited list of statuses from TAK026, to the most relevant SupervisionPeriodAdmissionReason, when possible. If the status list is empty, we assume that this period ended because the person transferred between POs or offices. """ if not label: raise ValueError( "Unexpected empty/null status list - empty values should not be passed to this mapper" ) if label == "TRANSFER WITHIN STATE": return StateSupervisionPeriodAdmissionReason.TRANSFER_WITHIN_STATE # TODO(#2865): Update enum normalization so that we separate by commas instead of spaces statuses = sorted_list_from_str(label, " ") def status_rank(status: str) -> int: """In the case that there are multiple statuses on the same day, we pick the status that is most likely to give us accurate info about the reason this supervision period was started. In the case of supervision period admissions, we pick statuses that have the pattern X5I* (e.g. '15I1000'), since those statuses are field (5) IN (I) statuses. In the absence if one of those statuses, we get our info from other statuses. """ if status not in INVESTIGATION_START_STATUSES: if re.match(TAK026_STATUS_SUPERVISION_PERIOD_START_REGEX, status): return 0 return 1 # Since we filter out all portions of supervision periods that happen before an initial investigation is over, # if we find a period that starts with one of these statuses, it means a new investigation happened to open up # on the same day as a person transferred POs. We generally want to ignore this case and just treat it as a # transfer unless there are other statuses that give us more info. return 2 sorted_statuses = sorted( statuses, key=lambda status: _status_rank_str(status, status_rank) ) for sp_admission_reason_str in sorted_statuses: if ( sp_admission_reason_str in STR_TO_SUPERVISION_PERIOD_ADMISSION_REASON_MAPPINGS ): return STR_TO_SUPERVISION_PERIOD_ADMISSION_REASON_MAPPINGS[ sp_admission_reason_str ] return StateSupervisionPeriodAdmissionReason.INTERNAL_UNKNOWN
def incarceration_period_admission_reason_mapper( status_list_str: str, ) -> StateIncarcerationPeriodAdmissionReason: """Converts a string with a list of TAK026 MO status codes into a valid incarceration period admission reason.""" start_statuses = sorted_list_from_str(status_list_str, " ") ranked_status_map: Dict[int, List[str]] = {} # First rank all statuses individually for status_str in start_statuses: status_rank = rank_incarceration_period_admission_reason_status_str(status_str) if status_rank is None: # If None, this is not an status code for determining the admission status continue if status_rank not in ranked_status_map: ranked_status_map[status_rank] = [] ranked_status_map[status_rank].append(status_str) if not ranked_status_map: # None of the statuses can meaningfully tell us what the admission reason is (rare) return StateIncarcerationPeriodAdmissionReason.INTERNAL_UNKNOWN # Find the highest order status(es) and use those to determine the admission reason highest_rank = sorted(list(ranked_status_map.keys()))[0] statuses_at_rank = ranked_status_map[highest_rank] potential_admission_reasons: Set[StateIncarcerationPeriodAdmissionReason] = set() for status_str in statuses_at_rank: if status_str not in STR_TO_INCARCERATION_PERIOD_ADMISSION_REASON_MAPPINGS: raise ValueError( f"No mapping for incarceration admission status {status_str}" ) potential_admission_reasons.add( STR_TO_INCARCERATION_PERIOD_ADMISSION_REASON_MAPPINGS[status_str] ) if potential_admission_reasons == { StateIncarcerationPeriodAdmissionReason.PROBATION_REVOCATION, StateIncarcerationPeriodAdmissionReason.PAROLE_REVOCATION, }: return StateIncarcerationPeriodAdmissionReason.DUAL_REVOCATION if len(potential_admission_reasons) > 1: raise EnumParsingError( StateIncarcerationPeriodAdmissionReason, f"Found status codes with conflicting information: [{statuses_at_rank}], which evaluate to " f"[{potential_admission_reasons}]", ) return one(potential_admission_reasons)
def supervision_period_termination_reason_mapper( label: str, ) -> Optional[StateSupervisionPeriodTerminationReason]: """Maps |label|, a space delimited list of statuses from TAK026, to the most relevant SupervisionPeriodTerminationReason, when possible. If the status list is empty, we assume that this period ended because the person transferred between POs or offices. """ if not label: raise ValueError( "Unexpected empty/null status list - empty values should not be passed to this mapper" ) if label == "TRANSFER WITHIN STATE": return StateSupervisionPeriodTerminationReason.TRANSFER_WITHIN_STATE # TODO(#2865): Update enum normalization so that we separate by commas instead of spaces statuses = sorted_list_from_str(label, " ") def status_rank(status: str) -> int: """In the case that there are multiple statuses on the same day, we pick the status that is most likely to give us accurate info about the reason this supervision period was terminated. In the case of supervision period terminations, we pick statuses first that have the pattern 99O* (e.g. '99O9020'), since those statuses always end a whole offender cycle, then statuses with pattern 95O* (sentence termination), then finally X5O*, since those statuses are field (5) OUT (O) statuses. In the absence if one of those statuses, we get our info from other statuses. """ if re.match(TAK026_STATUS_CYCLE_TERMINATION_REGEX, status): return 0 if re.match(TAK026_STATUS_SUPERVISION_SENTENCE_COMPLETION_REGEX, status): return 1 if re.match(TAK026_STATUS_SUPERVISION_PERIOD_TERMINATION_REGEX, status): return 2 return 3 sorted_statuses = sorted( statuses, key=lambda status: _status_rank_str(status, status_rank) ) for sp_termination_reason_str in sorted_statuses: if ( sp_termination_reason_str in STR_TO_SUPERVISION_PERIOD_TERMINATION_REASON_MAPPINGS ): return STR_TO_SUPERVISION_PERIOD_TERMINATION_REASON_MAPPINGS[ sp_termination_reason_str ] return StateSupervisionPeriodTerminationReason.INTERNAL_UNKNOWN
def _hydrate_violation_report_fields(_file_tag: str, row: Dict[str, str], extracted_objects: List[IngestObject], _cache: IngestObjectCache): """Adds fields/children to the SupervisionViolationResponses as necessary. This assumes all SupervisionViolationResponses are of violation reports. """ recommendations = set( sorted_list_from_str( row.get('parolee_placement_recommendation', '')) + sorted_list_from_str( row.get('probationer_placement_recommendation', ''))) for obj in extracted_objects: if isinstance(obj, StateSupervisionViolationResponse): obj.response_type = StateSupervisionViolationResponseType.VIOLATION_REPORT.value for recommendation in recommendations: if recommendation in VIOLATION_REPORT_NO_RECOMMENDATION_VALUES: continue recommendation_to_create = StateSupervisionViolationResponseDecisionEntry( decision=recommendation) create_if_not_exists( recommendation_to_create, obj, 'state_supervision_violation_response_decisions')
def _hydrate_violation_types(_file_tag: str, row: Dict[str, str], extracted_objects: List[IngestObject], _cache: IngestObjectCache): """Adds ViolationTypeEntries onto the already generated SupervisionViolations.""" violation_types = sorted_list_from_str(row.get('violation_types', '')) if not violation_types: return for obj in extracted_objects: if isinstance(obj, StateSupervisionViolation): for violation_type in violation_types: violation_type_to_create = StateSupervisionViolationTypeEntry( violation_type=violation_type) create_if_not_exists(violation_type_to_create, obj, 'state_supervision_violation_types')
def _set_violation_violent_sex_offense( _file_tag: str, row: Dict[str, str], extracted_objects: List[IngestObject], _cache: IngestObjectCache): """Sets the fields `is_violent` and `is_sex_offense` onto StateSupervisionViolations based on fields passed in through the |row|. """ new_crime_types = sorted_list_from_str(row.get('new_crime_types', '')) if not all(ct in ALL_NEW_CRIME_TYPES for ct in new_crime_types): raise ValueError(f'Unexpected new crime type: {new_crime_types}') violent = any([ct in VIOLENT_CRIME_TYPES for ct in new_crime_types]) sex_offense = any([ct in SEX_CRIME_TYPES for ct in new_crime_types]) for obj in extracted_objects: if isinstance(obj, StateSupervisionViolation): obj.is_violent = str(violent) obj.is_sex_offense = str(sex_offense)
def purpose_for_incarceration_mapper( label: str) -> Optional[StateSpecializedPurposeForIncarceration]: """Parses status information from the 'offstat' table into potential purposes for incarceration. Ranking of priority is taken from Idaho itself (the 'statstrt' table in the us_id_raw_data dataset). """ statuses = sorted_list_from_str(label, delimiter=' ') if 'TM' in statuses: # Termer return StateSpecializedPurposeForIncarceration.GENERAL if 'RJ' in statuses: # Rider return StateSpecializedPurposeForIncarceration.TREATMENT_IN_PRISON if 'NO' in statuses: # Non Idaho commitment TODO(3518): Consider adding as specialized purpose. return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if 'PV' in statuses: # Parole board hold return StateSpecializedPurposeForIncarceration.PAROLE_BOARD_HOLD if 'IP' in statuses: # Institutional Probation TODO(3518): Understand what this is. return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if 'CV' in statuses: # Civil commitment TODO(3518): Consider adding a specialized purpose for this return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN if 'CH' in statuses: # Courtesy Hold TODO(3518): Understand what this is return StateSpecializedPurposeForIncarceration.INTERNAL_UNKNOWN return None