Exemple #1
0
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
Exemple #2
0
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
Exemple #3
0
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
Exemple #4
0
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
Exemple #5
0
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.",
    )
Exemple #6
0
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"
Exemple #8
0
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],
    )
Exemple #10
0
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]
Exemple #11
0
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
Exemple #12
0
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
Exemple #13
0
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
Exemple #14
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
Exemple #15
0
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
Exemple #16
0
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
Exemple #17
0
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
Exemple #18
0
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)
Exemple #19
0
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