def test_add_new_charge(): record, questions = RecordCreator.build_record( search("single_case_two_charges"), "username", "password", (), { "X0001": { "summary": { "edit_status": "UPDATE" }, "charges": { "X0001-3": { "edit_status": "ADD", "charge_type": "MisdemeanorClassA", "level": "Misdemeanor Class A", "date": "1/1/2001", "disposition": { "date": "2/1/2020", "ruling": "Convicted" }, } }, } }, date.today(), LRUCache(4), ) assert isinstance(record.cases[0].charges[2].charge_type, MisdemeanorClassA) assert record.cases[0].charges[2].date == date(2001, 1, 1) assert record.cases[0].charges[2].edit_status == EditStatus.ADD
def test_add_disposition(): record, questions = RecordCreator.build_record( search("single_case_two_charges"), "username", "password", (), { "X0001": { "summary": { "edit_status": "UPDATE" }, "charges": { "X0001-2": { "disposition": { "date": "1/1/2001", "ruling": "Convicted" }, "level": "Misdemeanor Class A", } }, } }, date.today(), LRUCache(4), ) assert record.cases[0].charges[ 1].disposition.status == DispositionStatus.CONVICTED assert record.cases[0].charges[1].edit_status == EditStatus.UNCHANGED
def test_edit_charge_type_of_charge(): record, questions = RecordCreator.build_record( search("single_case_two_charges"), "username", "password", (), { "X0001": { "summary": { "edit_status": "UPDATE" }, "charges": { "X0001-2": { "edit_status": "UPDATE", "charge_type": "MisdemeanorClassA", "level": "Misdemeanor Class A", } }, } }, date.today(), LRUCache(4), ) assert isinstance(record.cases[0].charges[1].charge_type, MisdemeanorClassA)
class Demo(MethodView): search_cache = LRUCache(4) def post(self): record_summary = self.build_response() response_data = {"record": record_summary} return json.dumps(response_data, cls=ExpungeModelEncoder) def build_response(self): request_data = request.get_json() Search._validate_request(request_data) today = Search._build_today(request_data.get("today", "")) return Demo._build_record_summary(request_data["aliases"], request_data.get("questions"), request_data.get("edits", {}), today) @staticmethod def _build_record_summary(aliases_data, questions_data, edits_data, today): aliases = [ from_dict(data_class=Alias, data=alias) for alias in aliases_data ] record, questions = RecordCreator.build_record( DemoRecords.build_search_results, "username", "password", tuple(aliases), edits_data, today, Demo.search_cache, ) if questions_data: questions = Search._build_questions(questions_data) return RecordSummarizer.summarize(record, questions)
def test_edit_some_fields_on_case(): record, questions = RecordCreator.build_record( search("two_cases_two_charges_each"), "username", "password", (), { "X0002": { "summary": { "edit_status": "UPDATE", "location": "ocean", "balance_due": "100", "date": "1/1/1981", } } }, date.today(), LRUCache(4), ) assert len(record.cases) == 2 assert record.cases[0].summary.location == "earth" assert record.cases[0].summary.edit_status == EditStatus.UNCHANGED assert record.cases[1].summary.location == "ocean" assert record.cases[1].summary.balance_due_in_cents == 10000 assert record.cases[1].summary.date == date(1981, 1, 1) assert record.cases[1].summary.edit_status == EditStatus.UPDATE
def create_ambiguous_record_with_questions( record=JohnDoe.RECORD_WITH_CLOSED_CASES, cases={ "X0001": CaseDetails.case_x(), "X0002": CaseDetails.case_x(), "X0003": CaseDetails.case_x() }, ) -> Tuple[Record, Dict[str, QuestionSummary]]: base_url = "https://publicaccess.courts.oregon.gov/PublicAccessLogin/" with requests_mock.Mocker() as m: m.post(URL.login_url(), text=PostLoginPage.POST_LOGIN_PAGE) m.post("{}{}".format(base_url, "Search.aspx?ID=100"), [{ "text": SearchPageResponse.RESPONSE }, { "text": record }]) for key, value in cases.items(): m.get("{}{}{}".format(base_url, "CaseDetail.aspx?CaseID=", key), text=value) aliases = (Alias(first_name="John", last_name="Doe", middle_name="", birth_date=""), ) return RecordCreator.build_record( RecordCreator.build_search_results, "username", "password", aliases, {}, date_class.today(), LRUCache(4))
def test_update_case_with_add_and_update_and_delete_charges(): record, questions = RecordCreator.build_record( search("single_case_two_charges"), "username", "password", (), { "X0001": { "summary": { "case_number": "X0001", "edit_status": "UPDATE", "location": "ocean", "balance_due": "100", "date": "1/1/1981", }, "charges": { "X0001-1": { "edit_status": "UPDATE", "charge_type": "FelonyClassB", "level": "Felony Class B", "date": "1/1/2001", "disposition": { "date": "2/1/2020", "ruling": "Convicted" }, }, "X0001-2": { "edit_status": "DELETE" }, "X0001-3": { "edit_status": "ADD", "charge_type": "FelonyClassC", "date": "1/1/1900", "level": "Felony Class A", "disposition": { "date": "2/1/1910", "ruling": "Convicted" }, }, }, } }, date.today(), LRUCache(4), ) assert len(record.cases) == 1 assert record.cases[0].summary.location == "ocean" assert record.cases[0].summary.edit_status == EditStatus.UPDATE assert record.cases[0].charges[0].ambiguous_charge_id == "X0001-1" assert record.cases[0].charges[0].edit_status == EditStatus.UPDATE assert isinstance(record.cases[0].charges[0].charge_type, FelonyClassB) assert record.cases[0].charges[1].ambiguous_charge_id == "X0001-2" assert record.cases[0].charges[1].edit_status == EditStatus.DELETE assert record.cases[0].charges[2].ambiguous_charge_id == "X0001-3" assert record.cases[0].charges[2].edit_status == EditStatus.ADD
def test_no_op(): record, questions = RecordCreator.build_record( search("two_cases_two_charges_each"), "username", "password", (), {}, date.today(), LRUCache(4)) assert len(record.cases) == 2 assert len(record.cases[0].charges) == 2 assert record.cases[1].charges[1].ambiguous_charge_id == "X0002-2" assert record.cases[1].charges[ 1].disposition.status == DispositionStatus.UNKNOWN assert record.cases[1].summary.edit_status == EditStatus.UNCHANGED assert isinstance(record.cases[1].charges[0].charge_type, FelonyClassB) assert (record.cases[1].charges[0].expungement_result.charge_eligibility. status == ChargeEligibilityStatus.ELIGIBLE_NOW) assert record.cases[1].charges[ 0].expungement_result.time_eligibility.status == EligibilityStatus.ELIGIBLE
class Search(MethodView): search_cache = LRUCache(4) def post(self): record_summary = self.build_response() response_data = {"record": record_summary} return json.dumps(response_data, cls=ExpungeModelEncoder, sort_keys=False) def build_response(self) -> RecordSummary: request_data = request.get_json() Search._validate_request(request_data) username, password = Search._oeci_login_params(request) return Search._build_record_summary( username, password, request_data["aliases"], request_data.get("questions"), request_data.get("edits", {}) ) @staticmethod def _build_record_summary(username, password, aliases_data, questions_data, edits_data) -> RecordSummary: aliases = [from_dict(data_class=Alias, data=alias) for alias in aliases_data] record, questions = RecordCreator.build_record( RecordCreator.build_search_results, username, password, tuple(aliases), edits_data, Search.search_cache ) if questions_data: questions = Search._build_questions(questions_data) return RecordSummarizer.summarize(record, questions) @staticmethod def _build_questions(questions_data): questions_as_list = [ from_dict(data_class=QuestionSummary, data=question) for id, question in questions_data.items() ] return dict(list(map(lambda q: (q.ambiguous_charge_id, q), questions_as_list))) @staticmethod def _oeci_login_params(request): cipher = DataCipher(key=current_app.config.get("SECRET_KEY")) if not "oeci_token" in request.cookies.keys(): error(401, "Missing login credentials to OECI.") decrypted_credentials = cipher.decrypt(request.cookies["oeci_token"]) return decrypted_credentials["oeci_username"], decrypted_credentials["oeci_password"] @staticmethod def _validate_request(request_data): check_data_fields(request_data, ["aliases"]) for alias in request_data["aliases"]: check_data_fields(alias, ["first_name", "last_name", "middle_name", "birth_date"])
def test_add_case(): record, questions = RecordCreator.build_record( search("single_case_two_charges"), "username", "password", (), { "5": { "summary": { "case_number": "5", "edit_status": "ADD", "location": "ocean", "balance_due": "100", "date": "1/1/1981", }, "charges": { "5-1": { "edit_status": "ADD", "charge_type": "FelonyClassC", "level": "Felony Class C", "date": "1/1/2001", "disposition": { "date": "2/1/2020", "ruling": "Convicted" }, } }, } }, date.today(), LRUCache(4), ) assert len(record.cases) == 2 assert record.cases[0].summary.location == "earth" assert record.cases[0].summary.edit_status == EditStatus.UNCHANGED assert record.cases[1].summary.location == "ocean" assert record.cases[1].summary.balance_due_in_cents == 10000 assert record.cases[1].summary.date == date(1981, 1, 1) assert record.cases[1].summary.edit_status == EditStatus.ADD assert record.cases[1].charges[0].edit_status == EditStatus.ADD assert record.cases[1].charges[0].charge_type assert isinstance(record.cases[1].charges[0].charge_type, FelonyClassC)
def test_delete_case(): record, questions = RecordCreator.build_record( search("two_cases_two_charges_each"), "username", "password", (), {"X0001": { "summary": { "edit_status": "DELETE" } }}, LRUCache(4), ) assert record.cases[0].summary.case_number == "X0001" assert record.cases[0].summary.edit_status == EditStatus.DELETE assert len(record.cases[0].charges) == 2 assert record.cases[0].charges[0].edit_status == EditStatus.DELETE assert record.cases[0].charges[1].edit_status == EditStatus.DELETE assert record.cases[1].summary.case_number == "X0002" assert record.cases[1].summary.edit_status == EditStatus.UNCHANGED
def test_deleted_charge_does_not_block(): """""" record, questions = RecordCreator.build_record( search("two_cases_two_charges_each"), "username", "password", (), { "X0001": { "summary": { "edit_status": "UPDATE" }, "charges": { "X0001-1": { "edit_status": "UPDATE", "date": "1/1/2020", "disposition": { "date": "2/1/2020", "ruling": "Convicted" }, "level": "Misdemeanor Class A", } }, }, }, date.today(), LRUCache(4), ) assert record.cases[0].summary.case_number == "X0001" assert record.cases[0].summary.edit_status == EditStatus.UPDATE assert record.cases[0].charges[0].edit_status == EditStatus.UPDATE assert record.cases[0].charges[1].edit_status == EditStatus.UNCHANGED assert record.cases[1].summary.case_number == "X0002" assert (record.cases[1].charges[0].expungement_result.charge_eligibility. status == ChargeEligibilityStatus.WILL_BE_ELIGIBLE) record, questions = RecordCreator.build_record( search("two_cases_two_charges_each"), "username", "password", (), { "X0001": { "summary": { "edit_status": "UPDATE", }, "charges": { "X0001-1": { "edit_status": "DELETE" }, "X0001-2": { "edit_status": "DELETE" }, }, } }, date.today(), LRUCache(4), ) assert record.cases[0].summary.case_number == "X0001" assert record.cases[0].summary.edit_status == EditStatus.UPDATE assert record.cases[0].charges[0].edit_status == EditStatus.DELETE assert record.cases[0].charges[1].edit_status == EditStatus.DELETE assert record.cases[1].summary.case_number == "X0002" assert (record.cases[1].charges[0].expungement_result.charge_eligibility. status == ChargeEligibilityStatus.ELIGIBLE_NOW)
class Crawler: cached_links = LRUCache(1000) @staticmethod def attempt_login(session: Session, username, password) -> str: url = URL.login_url() payload = Payload.login_payload(username, password) response = session.post(url, data=payload) if Crawler._succeed_login(response): return response.text elif "Oregon eCourt is temporarily unavailable due to maintenance" in response.text: raise OECIUnavailable else: raise InvalidOECIUsernamePassword @staticmethod def fetch_link(link: str, session: Session = None): if session: response = session.get(link) Crawler.cached_links[link] = response return response else: return Crawler.cached_links[link] @staticmethod def search(session: Session, login_response, first_name, last_name, middle_name="", birth_date="") -> List[OeciCase]: search_url = URL.search_url() node_response = Crawler._fetch_search_page(session, search_url, login_response) oeci_search_result = Crawler._search_record(session, node_response, search_url, first_name, last_name, middle_name, birth_date) case_limit = 300 if len(oeci_search_result.cases) >= case_limit: raise ValueError( f"Found {len(oeci_search_result.cases)} matching cases, exceeding the limit of {case_limit}. Please add a date of birth to your search." ) else: # Parse search results (case detail pages) with ThreadPoolExecutor(max_workers=50) as executor: oeci_cases: List[OeciCase] = [] for oeci_case in executor.map( partial(Crawler._read_case, session), oeci_search_result.cases): oeci_cases.append(oeci_case) return oeci_cases @staticmethod def _search_record(session: Session, node_response, search_url, first_name, last_name, middle_name, birth_date): payload = Crawler.__extract_payload(node_response, last_name, first_name, middle_name, birth_date) response = session.post(search_url, data=payload, timeout=30) record_parser = RecordParser() record_parser.feed(response.text) return record_parser @staticmethod def _read_case(session: Session, case_summary: CaseSummary) -> OeciCase: case_parser_data = Crawler._parse_case(session, case_summary) balance_due_in_cents = CaseCreator.compute_balance_due_in_cents( case_parser_data.balance_due) charges: List[OeciCharge] = [] for charge_id, charge_dict in case_parser_data.hashed_charge_data.items( ): ambiguous_charge_id = f"{case_summary.case_number}-{charge_id}" charge = Crawler._build_oeci_charge(charge_id, ambiguous_charge_id, charge_dict, case_parser_data, balance_due_in_cents) charges.append(charge) updated_case_summary = replace( case_summary, balance_due_in_cents=balance_due_in_cents, edit_status=EditStatus.UNCHANGED) return OeciCase(updated_case_summary, charges=tuple(charges)) @staticmethod def _fetch_search_page(session, url, login_response): node_parser = NodeParser() node_parser.feed(login_response) payload = {"NodeID": node_parser.node_id, "NodeDesc": "All+Locations"} return session.post(url, data=payload) @staticmethod def _parse_case(session: Session, case: CaseSummary): response = Crawler.fetch_link(case.case_detail_link, session) if response.status_code == 200 and response.text: return CaseParser.feed(response.text) else: raise ValueError( f"Failed to fetch case detail page. Please rerun the search.")