def test_no_paramilitary_training(example_crecord): example_crecord.cases[0].charges = [ Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="18 § 5515", sentences=[], ) ] d = no_paramilitary_training(example_crecord, conviction_limit=1, within_years=15) assert bool(d) is False example_crecord.cases[0].disposition_date = date(1950, 1, 1) d = no_paramilitary_training(example_crecord, conviction_limit=1, within_years=15) assert bool(d) is True example_crecord.cases[0].disposition_date = date.today() example_crecord.cases[0].charges.append( Charge( offense="eating ice cream", grade="M1", disposition="Guilty Plea", statute="18 § 1", sentences=[], )) d = no_paramilitary_training(example_crecord, conviction_limit=1, within_years=15) assert bool(d) is False
def test_partial_seal(example_crecord): example_crecord.cases[0].total_fines = 0 example_crecord.cases[0].disposition_date = date(1990, 1, 1) example_crecord.cases[0].charges[0] = Charge( offense="Being silly", grade="M1", disposition="Guilty", disposition_date=date(2010, 1, 1), statute="14 s 123", sentences=[], ) new_charge = Charge( offense="Overzealous puzzle-assembling, using a firearm", grade="S", disposition="Guilty", statute="18 § 6101", sentences=[], ) example_crecord.cases[0].charges.append(new_charge) mod_rec, analysis = seal_convictions(example_crecord) assert "puzzle-assembling" in mod_rec.cases[0].charges[0].offense petition = analysis.value[0] assert isinstance(petition, Petition) assert "silly" in petition.cases[0].charges[0].offense
def test_no_danger_to_person_offense(example_crecord): example_crecord.cases[0].charges[0] = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="18 § 123", sentences=[], ) decision = no_danger_to_person_offense(example_crecord, penalty_limit=7, conviction_limit=1, within_years=20) assert bool(decision.value) == True charge = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="18 § 2301", sentences=[], ) decision = no_danger_to_person_offense(charge, penalty_limit=2, conviction_limit=1, within_years=20) assert bool(decision.value) == False
def no_corruption_of_minors_offense( charge: Charge, penalty_limit: int, conviction_limit: int, within_years: int ) -> Decision: """ No disqualifying convictions for corruption of minors. No sealing a conviction if it was punishable by more than two years for offenses under section 6301(a)(1), corruption of minors. 18 Pa.C.S. 9122.1(b)(1)(v) Returns: a True Decision if the charge was NOT a disqualifying offense. Otherwise a False Decision. """ decision = Decision(name="This charge is not a disqualifying corruption of minors offense?") patt = re.compile(r"^(?P<chapt>\d+)\s*§\s(?P<section>\d+\.?\d*)\s*(?P<subsections>[\(\)A-Za-z0-9\.]+).*") matches = patt.match(charge.statute) if not matches: decision.reasoning = "This doesn't appear to be one of the tiered sex offense statutes." decision.value = True else: this_offense = matches.group("section") + matches.group("subsections").replace("(","").replace(")","") decision.reasoning = [charge.is_conviction(), charge.get_statute_chapter() == 18, this_offense == "6301a1"] decision.value = not all(decision.reasoning) return decision
def is_conviction(charge: Charge) -> Decision: return Decision( name=f"Is this charge for {charge.offense} a conviction?", value=charge.is_conviction(), reasoning= f"The charge's disposition {charge.disposition} indicates a conviction" if charge.is_conviction() else f"The charge's disposition {charge.disposition} indicates its not a conviction.", )
def test_no_offense_against_family(example_crecord): charge = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="18 § 4301", sentences=[], ) example_crecord.cases[0].charges[0] = charge d = no_offense_against_family(example_crecord, penalty_limit=1, conviction_limit=1, within_years=50) assert bool(d) is False d = no_offense_against_family(example_crecord, penalty_limit=1, conviction_limit=3, within_years=50) assert bool(d) is True example_crecord.cases[0].charges = [charge, charge, charge, charge] d = no_offense_against_family(example_crecord, penalty_limit=1, conviction_limit=2, within_years=50) assert bool(d) is False
def test_case(example_sentence): char = Charge( "Eating w/ mouth open", "M2", "14 section 23", "Guilty Plea", sentences=[example_sentence], ) case = Case( status="Open", county="Erie", docket_number="12-CP-02", otn="112000111", dc="11222", charges=[char], total_fines=200, fines_paid=1, judge="Smooth Operator", judge_address="1234 Other St., PA", disposition_date=None, arrest_date=None, complaint_date=None, affiant="Sheriff Smelly", arresting_agency="Upsidedown County", arresting_agency_address="1234 Main St., PA", ) assert case.status == "Open"
def not_felony1(charge: Charge) -> Decision: """ Any F1 graded offense disqualifies a whole record from sealing. 18 PA Code 9122.1(b)(2)(i) Returns: a True decision if the charge was NOT a felony1 conviction. """ decision = Decision(name="Is the charge an F1 conviction?") if charge.grade.strip() == "": decision.value = False decision.reasoning = ( "The charge's grade is unknown, so we don't know its *not* an F1." ) elif re.match("F1", charge.grade): if charge.is_conviction(): decision.value = False decision.reasoning = "The charge is an F1 conviction" else: decision.value = True decision.reasoning = ( f"The charge was F1, but the disposition was {charge.disposition}" ) else: decision.value = True decision.reasoning = f"The charge is {charge.grade}, which is not F1" return decision
def example_charge(example_sentence): return Charge( "Eating w/ mouth open", "M2", "14 section 23", "Guilty Plea", disposition_date=date(2010, 1, 1), sentences=[example_sentence], )
def get_charges(stree: etree) -> List[Charge]: """ Find a list of the charges in a parsed docket. """ # find the charges in the Charges section charges = stree.xpath("//section[@name='section_charges']//charge") # charges is temporarily a list of tuples of [(sequence_num, Charge)] charges = [( xpath_or_blank(charge, "./seq_num"), Charge( offense=xpath_or_blank(charge, "./statute_description"), grade=xpath_or_blank(charge, "./grade"), statute=xpath_or_blank(charge, "./statute"), disposition="Unknown", disposition_date=None, sentences=[], ), ) for charge in charges] # figure out the disposition dates by looking for a final disposition date that matches a charge. final_disposition_events = stree.xpath( "//section[@name='section_disposition_sentencing']//case_event[case_event_desc_and_date/is_final[contains(text(),'Final Disposition')]]" ) for final_disp_event in final_disposition_events: final_disp_date = xpath_date_or_blank(final_disp_event, ".//case_event_date") applies_to_sequences = xpath_or_empty_list(final_disp_event, ".//sequence_number") for seq_num in applies_to_sequences: # set the final_disp date for the charge with sequence number seq_num for sn, charge in charges: if sn == seq_num: charge.disposition_date = final_disp_date # Figure out the disposition of each charge from the disposition section. # Do this by finding the last sequence in the disposition section for # the sequence with seq_num. The disposition of the charge is that # sequence's disposition. Sentence is in that xml element too. try: disposition_section = stree.xpath( "//section[@name='section_disposition_sentencing']")[0] for seq_num, charge in charges: try: # seq is the last sequence for the charge seq_num. seq = disposition_section.xpath( f"./disposition_section/disposition_subsection/disposition_details/case_event/sequences/sequence[sequence_number/text()=' {seq_num} ']" )[-1] charge.disposition = xpath_or_blank(seq, "./offense_disposition") charge.sentences = get_sentences(seq) except IndexError: continue except IndexError: pass return [c for i, c in charges]
def test_offenses_punishable_by_two_or_more_years(example_crecord): example_crecord.cases[0].disposition_date = date.today() c1 = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="18 § 2301", sentences=[], ) c2 = Charge( offense="Being sleepy", grade="F2", disposition="Guilty", statute="18 § 0987", sentences=[], ) c3 = Charge( offense="Being hungry", grade="M2", disposition="Guilty", statute="18 § 1111", sentences=[], ) c4 = Charge( offense="Being funny", grade="M2", disposition="Guilty", statute="18 § 1111", sentences=[], ) example_crecord.cases[0].charges = [c1, c2, c3, c4] assert bool( offenses_punishable_by_two_or_more_years(example_crecord, conviction_limit=4, within_years=20)) == False example_crecord.cases[0].disposition_date = date(1950, 1, 1) assert bool( offenses_punishable_by_two_or_more_years(example_crecord, conviction_limit=4, within_years=20)) == True
def test_seal(example_crecord): example_crecord.cases[0].fines_and_costs = 0 example_crecord.cases[0].disposition_date = date(1990, 1, 1) example_crecord.cases[0].charges[0] = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="14 s 123", sentences=[], ) mod_rec, analysis = seal_convictions(example_crecord, Decision("Seal convictions")) res = json.dumps(analysis, default=to_serializable, indent=4) assert len(analysis["Seal Convictions"].value["sealings"]) == 1
def test_seal(example_crecord): example_crecord.cases[0].total_fines = 0 example_crecord.cases[0].disposition_date = date(1990, 1, 1) example_crecord.cases[0].charges[0] = Charge( offense="Being silly", grade="M1", disposition="Guilty", statute="14 s 123", sentences=[], ) mod_rec, analysis = seal_convictions(example_crecord) assert len(analysis.value) == 1 assert isinstance(analysis.value[0], Sealing) assert len(mod_rec.cases) == 0
def is_felony_conviction(charge: Charge) -> Decision: """ Was `charge` a felony conviction Args: charge: A Charge. Return: A Decision that is True if the charge is a felony conviction. """ decision = Decision(name=f"Was the charge [{charge.offense}, {charge.grade}, {charge.disposition}] a felony conviction?") decision.reasoning = [ re.match("F", charge.grade, re.IGNORECASE), charge.is_conviction(), ] decision.value = all(decision.reasoning) return decision
def not_murder(charge: Charge) -> Decision: """ Checks if a charge was a conviction for murder. Returns true if the charge was NOT a murder conviction. TODO The Expungement Generator's test is for the statute 18 PaCS 1502. Does the implementation here even work? Need to find real murder convictions to see. """ decision = Decision(name="Is the charge NOT a murder conviction?") if charge.is_conviction(): if re.match("murder", charge.offense, re.IGNORECASE): decision.value = False decision.reasoning = "The charge was a murder conviction." else: decision.value = True decision.reasoning = "Conviction for something other than murder." else: decision.value = True decision.reasoning = "Not a conviction." return decision
def get_md_cases(summary_xml: etree.Element) -> List: """ Return a list of the cases described in this Summary sheet. """ cases = [] case_elements = summary_xml.xpath("//case") for case in case_elements: # in mdj summaries, there's only one "charge" element, not different "open" and "closed" elements. # And there are no sentences recorded. md_charges = [] md_charge_elems = case.xpath(".//charge") for charge in md_charge_elems: charge = Charge( offense=text_or_blank(charge.find("description")), statute=text_or_blank(charge.find("statute")), grade=text_or_blank(charge.find("grade")), disposition=text_or_blank(charge.find("disposition")), sentences=[], ) md_charges.append(charge) cases.append( Case( status=text_or_blank(case.getparent().getparent()), county=text_or_blank(case.getparent().find("county")), docket_number=text_or_blank(case.find("case_basics/docket_num")), otn=text_or_blank(case.find("case_basics/otn_num")), dc=text_or_blank(case.find("case_basics/dc_num")), charges=md_charges, total_fines=None, # a summary docket never has info about this. fines_paid=None, arrest_date=date_or_none( case.find("arrest_disp_actions/arrest_disp/arrest_date") ), disposition_date=date_or_none( case.find("arrest_disp_actions/arrest_disp/disp_date") ), judge=text_or_blank( case.find("arrest_disp_actions/arrest_disp/disp_judge") ), ) ) return cases
def parse_charges(txt: str) -> Tuple[Optional[List[Charge]], List[str]]: """ Find the charges in the text of a docket. Returns: Tuple[0] is either None or a list of Charges. Tuple[1] is a list of strings describing errors encountered. """ logger.info(" parsing charges") disposition_section_searcher = re.compile( r"(?:.*\s+)DISPOSITION SENTENCING/PENALTIES\s*\n(?P<disposition_section>(.+\n+(?=[A-Z ]+))+.*)" ) errs = [] disposition_sections = disposition_section_searcher.findall(txt) if disposition_section_searcher == []: errs.append("Could not find the disposition/sentencing section.") return None, errs charges = [] charges_pattern = r"(?P<sequence>\d)\s+\/\s+(?P<offense>.+)\s{12,}(?P<disposition>\w.+?)(?=\s\s)\s{12,}(?P<grade>\w{0,2})\s+(?P<statute>\w{1,2}\s?\u00A7\s?\d+(\-|\u00A7|\w+)*)" # there may be multiple disposition sections for disposition_section in disposition_sections: section_text = disposition_section[0] section_lines = section_text.split("\n") for idx, ln in enumerate(section_lines): # Need to use a copy of the index, to advance if we find a charge overflow line, so that # when we reach forward for the disposition date, we compensate if we've also found a charge overflow line. idx_copy = idx # not using the find_pattern function here because we're doing repeated searches on every line, # and failing to match is not an error, in that case. charge_line_search = re.search(charges_pattern, ln) if charge_line_search is not None: logger.debug(f"found a charge in line: {ln}") offense = charge_line_search.group("offense").strip() charge_overflow_search = re.search( r"^\s+(?P<offense_overflow>\w+\s*\w*)\s*$", section_lines[idx + 1], re.I, ) if charge_overflow_search is not None: offense += (" " + charge_overflow_search.group( "offense_overflow").strip()) idx_copy += 1 try: sequence = int( charge_line_search.group("sequence").strip()) except Exception: sequence = None charge = Charge( sequence=sequence, offense=offense, grade=charge_line_search.group("grade"), statute=charge_line_search.group("statute"), disposition=charge_line_search.group("disposition"), sentences= [], # TODO: re_parse_cp_pdf parser does not collect Sentences yet. ) # sometimes a single charge may have multiple successive disposition dates. We need the last one. next_line_index = idx_copy + 1 disp_date_search = re.search( r"(.*)\s(?P<disposition_date>\d{1,2}\/\d{1,2}\/\d{4})", section_lines[next_line_index], ) while re.search( r"(.*)\s(?P<disposition_date>\d{1,2}\/\d{1,2}\/\d{4})", section_lines[next_line_index], ): disp_date_search = re.search( r"(.*)\s(?P<disposition_date>\d{1,2}\/\d{1,2}\/\d{4})", section_lines[next_line_index], ) next_line_index += 1 # # disposition_date_line = section_lines[idx_copy + 1] # disp_date_search = re.search(r"(.*)\s(?P<disposition_date>\d{1,2}\/\d{1,2}\/\d{4})",disposition_date_line) if disp_date_search is not None: charge.disposition_date = date_or_none( disp_date_search.group("disposition_date")) if charge.disposition_date is None: errs.append( f"For the offense, {charge.sequence}/ {offense}, we found, but could not parse, the disposition date: {disp_date_search.group('disposition_date')}" ) charges.append(charge) charges = Charge.reduce_merge(charges) missing_disposition_dates = [ f"Could not find disposition date for {c.sequence} / {c.offense} with disposition {c.disposition}" for c in charges if c.disposition_date is None ] errs += missing_disposition_dates return charges, errs
def more_than_x_convictions_y_grade_z_years(crecord: CRecord, offense_limit: int, grade_limit: str, years: int) -> Decision: """ Does `crecord` contain equal or more than `offense_limit` convictions for `grade_limit` (or more serious) offenses in the last `years` years? The limits are inclusive. For example, `more_than_x_convictions_y_grade_z_years(rec, 2, M1, 15)` would return a Decision that explains whether `rec` contains 2 or more M1-or-greater convictions in the last 15 years. Args: crecord: A criminal record. offense_limit: Are there more convictions than this number in this record? grade_limit: The grade (i.e. M1) that triggers this rule years: Years since a conviction that will be counted. Returns: A decision that is True if `crecord` contains more than the `offense_limit` of `grade_limit` convictions in the last `years` years. """ decision = Decision(name=f"Does {crecord.person.full_name()}'s record contain {offense_limit} or more convictions, graded {grade_limit} or higher, within the last {years} years?") qualifying_charges = [] for case in crecord.cases: for charge in case.charges: if case.years_passed_disposition() >= years and charge.is_conviction() and Charge.grade_GTE(charge.grade, grade_limit): qualifying_charges.append(charge)
def get_cp_cases(summary_xml: etree.Element) -> List: """ Return a list of the cases described in this Summary sheet. """ cases = [] case_elements = summary_xml.xpath("//case") for case in case_elements: closed_sequences = case.xpath(".//closed_sequence") closed_charges = [] for seq in closed_sequences: charge = Charge( offense=text_or_blank(seq.find("description")), statute=text_or_blank(seq.find("statute")), grade=text_or_blank(seq.find("grade")), disposition=text_or_blank(seq.find("sequence_disposition")), disposition_date=None, sentences=[], ) for sentence in seq.xpath(".//sentencing_info"): charge.sentences.append( Sentence( sentence_date=date_or_none(sentence.find("sentence_date")), sentence_type=text_or_blank(sentence.find("sentence_type")), sentence_period=text_or_blank(sentence.find("program_period")), sentence_length=SentenceLength.from_tuples( min_time=( text_or_blank( sentence.find("sentence_length/min_length/time") ), text_or_blank( sentence.find("sentence_length/min_length/unit") ), ), max_time=( text_or_blank( sentence.find("sentence_length/max_length/time") ), text_or_blank( sentence.find("sentence_length/max_length/unit") ), ), ), ) ) closed_charges.append(charge) open_sequences = case.xpath(".//open_sequence") open_charges = [] for seq in open_sequences: charge = Charge( offense=text_or_blank(seq.find("description")), statute=text_or_blank(seq.find("statute")), grade=text_or_blank(seq.find("grade")), disposition=text_or_blank(seq.find("sequence_disposition")), disposition_date=None, sentences=[], ) for sentence in seq.xpath(".//sentencing_info"): charge.sentences.append( Sentence( sentence_date=date_or_none(sentence.find("sentence_date")), sentence_type=text_or_blank(sentence.find("sentence_type")), sentence_period=text_or_blank(sentence.find("program_period")), sentence_length=SentenceLength.from_tuples( min_time=( text_or_blank( sentence.find("sentence_length/min_length/time") ), text_or_blank( sentence.find("sentence_length/min_length/unit") ), ), max_time=( text_or_blank( sentence.find("sentence_length/max_length/time") ), text_or_blank( sentence.find("sentence_length/max_length/unit") ), ), ), ) ) open_charges.append(charge) new_case = Case( status=text_or_blank(case.getparent().getparent()), county=text_or_blank(case.getparent().find("county")), docket_number=text_or_blank(case.find("case_basics/docket_num")), otn=text_or_blank(case.find("case_basics/otn_num")), dc=text_or_blank(case.find("case_basics/dc_num")), charges=closed_charges + open_charges, total_fines=None, # a summary docket never has info about this. fines_paid=None, arrest_date=date_or_none( either( case.find("arrest_disp_actions/arrest_disp/arrest_date"), case.find("arrest_disp_actions/arrest_trial/arrest_date"), ) ), disposition_date=date_or_none( case.find("arrest_disp_actions/arrest_disp/disp_date") ), judge=text_or_blank( case.find("arrest_disp_actions/arrest_disp/disp_judge") ), ) # In Summaries, the Disposition Date is set on a Case, but it is set on a Charge in Dockets. # So when processing a Summary sheet, if there is a date on the Case, the Charges should # inherit the date on the case. for charge in new_case.charges: if new_case.disposition_date is not None: charge.disposition_date = new_case.disposition_date cases.append(new_case) return cases