示例#1
0
    def _form_summary_string(
            self,
            summary: TrafficViolationsAggregatorResponse) -> Optional[str]:

        num_vehicles = len(summary.plate_lookups)
        vehicle_tickets = [
            len(lookup.violations) for lookup in summary.plate_lookups
        ]
        total_tickets = sum(vehicle_tickets)

        fines_by_vehicle: List[FineData] = [
            lookup.fines for lookup in summary.plate_lookups
        ]

        vehicles_with_fines: int = len([
            fine_data for fine_data in fines_by_vehicle if fine_data.fined > 0
        ])

        aggregate_fines: FineData = FineData(
            **{
                field: sum(
                    getattr(lookup.fines, field)
                    for lookup in summary.plate_lookups)
                for field in FineData.FINE_FIELDS
            })

        if aggregate_fines.fined > 0:
            return [
                f"You queried {num_vehicles} vehicles, of which "
                f"{vehicles_with_fines} vehicle{L10N.pluralize(vehicles_with_fines)} "
                f"{'has' if vehicles_with_fines == 1 else 'have collectively'} received {total_tickets} ticket{L10N.pluralize(total_tickets)} "
                f"with at least {'${:,.2f}'.format(aggregate_fines.fined - aggregate_fines.reduced)} "
                f"in fines, of which {'${:,.2f}'.format(aggregate_fines.paid)} has been paid.\n\n"
            ]
示例#2
0
    def test_look_up_vehicle_with_no_violations(self,
                                            mocked_perform_query):

        plate = 'ABC1234'
        plate_types = 'PAS'
        state = 'NY'

        mocked_perform_query.side_effects = [{'data': {}} for _ in range(0, 8)]

        plate_query = PlateQuery(
            created_at='Tue Dec 31 19:28:12 -0500 2019',
            message_id=random.randint(
                1000000000000000000,
                2000000000000000000),
            message_source='status',
            plate=plate,
            plate_types=plate_types,
            state=state,
            username='******')

        result = OpenDataServiceResponse(
            data=OpenDataServicePlateLookup(
                boroughs=[],
                camera_streak_data=None,
                fines=FineData(fined=0, outstanding=0, paid=0, reduced=0),
                num_violations=0,
                plate=plate,
                plate_types=plate_types,
                state=state,
                violations=[],
                years=[]),
            success=True)

        self.assertEqual(self.open_data_service.look_up_vehicle(plate_query),
                         result)
示例#3
0
    def _form_plate_lookup_response_parts(
            self,
            borough_data: List[Tuple[str, int]],
            frequency: int,
            fine_data: FineData,
            plate: str,
            plate_types: List[str],
            state: str,
            unique_identifier: str,
            username: str,
            violations: List[Tuple[str, int]],
            year_data: List[Tuple[str, int]],
            camera_streak_data: Optional[CameraStreakData] = None,
            previous_lookup: Optional[PlateLookup] = None):

        # response_chunks holds tweet-length-sized parts of the response
        # to be tweeted out or appended into a single direct message.
        response_chunks: List[str] = []
        violations_string: str = ""

        # Get total violations
        total_violations: int = sum([s['count'] for s in violations])
        LOG.debug(f'total_violations: {total_violations}')

        # Append to initially blank string to build tweet.
        violations_string += L10N.LOOKUP_SUMMARY_STRING.format(
            L10N.VEHICLE_HASHTAG.format(state, plate),
            L10N.get_plate_types_string(plate_types), frequency,
            L10N.pluralize(int(frequency)))

        # If this vehicle has been queried before...
        if previous_lookup:
            previous_num_violations: int = previous_lookup.num_tickets

            violations_string += self._create_repeat_lookup_string(
                new_violations=(total_violations - previous_num_violations),
                plate=plate,
                previous_lookup=previous_lookup,
                state=state,
                username=username)

        response_chunks += self._handle_response_part_formation(
            collection=violations,
            continued_format_string=L10N.LOOKUP_TICKETS_STRING_CONTD.format(
                L10N.VEHICLE_HASHTAG.format(state, plate)),
            count='count',
            cur_string=violations_string,
            description='title',
            default_description='No Year Available',
            prefix_format_string=L10N.LOOKUP_TICKETS_STRING.format(
                total_violations),
            result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
            username=username)

        if year_data:
            response_chunks += self._handle_response_part_formation(
                collection=year_data,
                continued_format_string=L10N.LOOKUP_YEAR_STRING_CONTD.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                count='count',
                description='title',
                default_description='No Year Available',
                prefix_format_string=L10N.LOOKUP_YEAR_STRING.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
                username=username)

        if borough_data:
            response_chunks += self._handle_response_part_formation(
                collection=borough_data,
                continued_format_string=L10N.LOOKUP_BOROUGH_STRING_CONTD.
                format(L10N.VEHICLE_HASHTAG.format(state, plate)),
                count='count',
                description='title',
                default_description='No Borough Available',
                prefix_format_string=L10N.LOOKUP_BOROUGH_STRING.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
                username=username)

        if fine_data and fine_data.fines_assessed():

            cur_string = f"Known fines for {L10N.VEHICLE_HASHTAG.format(state, plate)}:\n\n"

            max_count_length = len('${:,.2f}'.format(fine_data.max_amount()))
            spaces_needed = (max_count_length * 2) + 1

            for fine_type, amount in fine_data:

                currency_string = '${:,.2f}'.format(amount)
                count_length = len(str(currency_string))

                # e.g., if spaces_needed is 5, and count_length is 2, we need
                # to pad to 3.
                left_justify_amount = spaces_needed - count_length

                # formulate next string part
                next_part = (f"{currency_string.ljust(left_justify_amount)}| "
                             f"{fine_type.replace('_', ' ').title()}\n")

                # determine current string length if necessary
                potential_response_length = len(username + ' ' + cur_string +
                                                next_part)

                # If username, space, violation string so far and new part are less or
                # equal than 280 characters, append to existing tweet string.
                if (potential_response_length <=
                        twitter_constants.MAX_TWITTER_STATUS_LENGTH):
                    cur_string += next_part
                else:
                    response_chunks.append(cur_string)

                    cur_string = "Known fines for #{}_{}, cont'd:\n\n"
                    cur_string += next_part

            # add to container
            response_chunks.append(cur_string)

        if camera_streak_data:

            if camera_streak_data.max_streak and camera_streak_data.max_streak >= 5:

                # formulate streak string
                streak_string = (
                    f"Under @bradlander's proposed legislation, "
                    f"this vehicle could have been booted or impounded "
                    f"due to its {camera_streak_data.max_streak} camera violations "
                    f"(>= 5/year) from {camera_streak_data.min_streak_date}"
                    f" to {camera_streak_data.max_streak_date}.\n")

                # add to container
                response_chunks.append(streak_string)

        unique_link: str = self._get_website_plate_lookup_link(
            unique_identifier)

        website_link_string = f'View more details at {unique_link}.'
        response_chunks.append(website_link_string)

        # Send it back!
        return response_chunks
示例#4
0
    def _calculate_aggregate_data(self, plate_query: PlateQuery,
                                  violations) -> OpenDataServicePlateLookup:
        # Marshal all ticket data into form.
        fines: FineData = FineData(fined=round(
            sum(v['fined'] for v in violations.values() if v.get('fined')), 2),
                                   reduced=round(
                                       sum(v['reduced']
                                           for v in violations.values()
                                           if v.get('reduced')), 2),
                                   paid=round(
                                       sum(v['paid']
                                           for v in violations.values()
                                           if v.get('paid')), 2),
                                   outstanding=round(
                                       sum(v['outstanding']
                                           for v in violations.values()
                                           if v.get('outstanding')), 2))

        tickets: List[Tuple[str, int]] = Counter([
            v['violation'].title() for v in violations.values()
            if v.get('violation') is not None
        ]).most_common()

        years: List[Tuple[str, int]] = Counter([
            datetime.strptime(v['issue_date'], self.TIME_FORMAT).strftime('%Y')
            if v.get('has_date') else 'No Year Available'
            for v in violations.values()
        ]).most_common()

        boroughs: List[Tuple[str, int]] = Counter([
            v['borough'].title() for v in violations.values()
            if v.get('borough')
        ]).most_common()

        camera_streak_data: Optional[
            CameraStreakData] = self._find_max_camera_violations_streak(
                sorted([
                    datetime.strptime(v['issue_date'], self.TIME_FORMAT)
                    for v in violations.values() if v.get('violation')
                    and v['violation'] in self.CAMERA_VIOLATIONS
                ]))

        return OpenDataServicePlateLookup(
            boroughs=[{
                'count': v,
                'title': k.title()
            } for k, v in boroughs],
            camera_streak_data=camera_streak_data,
            fines=fines,
            num_violations=len(violations),
            plate=plate_query.plate,
            plate_types=plate_query.plate_types,
            state=plate_query.state,
            violations=[{
                'count': v,
                'title': k.title()
            } for k, v in tickets],
            years=sorted([{
                'count': v,
                'title': k.title()
            } for k, v in years],
                         key=lambda k: k['title']))
    def test_look_up_vehicle_with_violations(self,
                                            mocked_perform_query,
                                            plate,
                                            state=None,
                                            medallion_query_result=None):

        random_state_index = random.randint(
            0, len(regexp_constants.STATE_ABBREVIATIONS) - 1)
        random_plate_type_index = random.randint(
            0, len(regexp_constants.PLATE_TYPES) - 1)

        final_plate = plate if medallion_query_result is None else medallion_query_result[0][
            'dmv_license_plate_number']
        plate_type = regexp_constants.PLATE_TYPES[random_plate_type_index]
        state = state if state else regexp_constants.STATE_ABBREVIATIONS[random_state_index]

        all_borough_codes = [code for borough_list in list(
            BOROUGH_CODES.values()) for code in borough_list]
        all_precincts = [precinct for sublist in list(
            PRECINCTS_BY_BOROUGH.values()) for precinct in sublist]

        summons_numbers = set()

        fiscal_year_databases_violations = []
        for year, _ in FISCAL_YEAR_DATABASE_ENDPOINTS.items():
            fy_violations = []
            for _ in range(random.randint(10, 20)):
                date = datetime(
                    year,
                    random.randint(1, 12),
                    random.randint(1, 28))

                random_borough_code_index = random.randint(
                    0, len(all_borough_codes) - 1)
                random_precinct_index = random.randint(
                    0, len(all_precincts) - 1)
                random_violation_string_index = random.randint(
                    0, len(HUMANIZED_NAMES_FOR_FISCAL_YEAR_DATABASE_VIOLATIONS.keys())-1)

                summons_number = random.randint(
                    1000000000,
                    9999999999)
                while summons_number in summons_numbers:
                    summons_number = random.randint(
                        1000000000,
                        9999999999)

                summons_numbers.add(summons_number)

                fy_violations.append({
                    'date_first_observed': date.strftime('%Y-%m-%dT%H:%M:%S.%f'),
                    'feet_from_curb': '0 ft',
                    'from_hours_in_effect': '0800',
                    'house_number': '123',
                    'intersecting_street': '',
                    'issue_date': date.strftime('%Y-%m-%dT%H:%M:%S.%f'),
                    'issuer_code': '43',
                    'issuer_command': 'C',
                    'issuer_precinct': all_precincts[random_precinct_index],
                    'issuing_agency': 'TRAFFIC',
                    'law_section': '123 ABC',
                    'plate_id': plate,
                    'plate_type': plate_type,
                    'registration_state': state,
                    'street_code1': '123',
                    'street_code2': '456',
                    'street_code3': '789',
                    'street_name': 'Fake Street',
                    'sub_division': '23',
                    'summons_image': '',
                    'summons_number': summons_number,
                    'to_hours_in_effect': '2200',
                    'vehicle_body_type': 'SUV',
                    'vehicle_color': 'BLACK',
                    'vehicle_expiration_date': date + timedelta(days=180),
                    'vehicle_make': 'FORD',
                    'vehicle_year': '2017',
                    'violation_code': list(HUMANIZED_NAMES_FOR_FISCAL_YEAR_DATABASE_VIOLATIONS.keys())[random_violation_string_index],
                    'violation_county': all_borough_codes[random_borough_code_index],
                    'violation_in_front_of_or_opposite': '',
                    'violation_legal_code': '4-08',
                    'violation_location': '',
                    'violation_post_code': '12345',
                    'violation_precinct': all_precincts[random_precinct_index]})

            fiscal_year_databases_violations.append(fy_violations)

        open_parking_and_camera_violations = []
        for _ in range(random.randint(10, 20)):
            fine_amount = random.randint(10, 200)
            interest_amount = round(float(random.randint(
                0, math.floor(fine_amount/2)) + random.random()), 2)
            penalty_amount = round(float(random.randint(
                0, math.floor(fine_amount/2)) + random.random()), 2)
            reduction_amount = round(float(random.randint(
                0, math.floor(fine_amount/2)) + random.random()), 2)

            payment_amount = round(float(random.randint(0, math.floor(
                fine_amount + interest_amount + penalty_amount - reduction_amount))), 2)
            amount_due = fine_amount + interest_amount + \
                penalty_amount - reduction_amount - payment_amount

            date = datetime(
                random.randint(1999, 2020),
                random.randint(1, 12),
                random.randint(1, 28))

            random_borough_code_index = random.randint(
                0, len(all_borough_codes) - 1)
            random_precinct_index = random.randint(
                0, len(all_precincts) - 1)
            random_violation_string_index = random.randint(
                0, len(HUMANIZED_NAMES_FOR_OPEN_PARKING_AND_CAMERA_VIOLATIONS.keys())-1)

            summons_number = random.randint(1000000000, 9999999999)
            summons_numbers.add(summons_number)

            open_parking_and_camera_violations.append({
                'amount_due': amount_due,
                'county': all_borough_codes[random_borough_code_index],
                'fine_amount': fine_amount,
                'interest_amount': interest_amount,
                'issue_date': date.strftime('%m/%d/%Y'),
                'issuing_agency': 'TRAFFIC',
                'license_type': plate_type,
                'payment_amount': payment_amount,
                'penalty_amount': penalty_amount,
                'plate': plate,
                'precinct': all_precincts[random_precinct_index],
                'reduction_amount': reduction_amount,
                'state': state,
                'summons': (f'http://nycserv.nyc.gov/NYCServWeb/'
                            f'ShowImage?searchID=VDBSVmQwNVVaekZ'
                            f'OZWxreFQxRTlQUT09&locationName='
                            f'_____________________'),
                'summons_image_description': 'View Summons',
                'summons_number': summons_number,
                'violation': list(
                    HUMANIZED_NAMES_FOR_OPEN_PARKING_AND_CAMERA_VIOLATIONS.keys())[
                        random_violation_string_index],
                'violation_status': 'HEARING HELD-GUILTY',
                'violation_time': f'0{random.randint(1,9)}:{random.randint(10,59)}P'
            })

        side_effects = []

        if medallion_query_result:
            side_effects.append({'data': medallion_query_result})

        side_effects.append({'data': copy.deepcopy(
            open_parking_and_camera_violations)})

        for violations_list in fiscal_year_databases_violations:
            side_effects.append({'data': copy.deepcopy(violations_list)})

        mocked_perform_query.side_effect = side_effects

        open_parking_and_camera_violations_dict = {}
        for summons in open_parking_and_camera_violations:
            summons['borough'] = [boro for boro, precincts in PRECINCTS_BY_BOROUGH.items(
            ) if int(summons['precinct']) in precincts][0]
            summons['has_date'] = True
            summons['issue_date'] = datetime.strptime(
                summons['issue_date'], '%m/%d/%Y').strftime('%Y-%m-%dT%H:%M:%S.%f')
            summons['violation'] = HUMANIZED_NAMES_FOR_OPEN_PARKING_AND_CAMERA_VIOLATIONS[summons['violation']]
            open_parking_and_camera_violations_dict[summons['summons_number']] = summons

        fiscal_year_databases_violations_dict = {}
        for violation_list in fiscal_year_databases_violations:
            for summons in violation_list:
                summons['borough'] = [boro for boro, precincts in PRECINCTS_BY_BOROUGH.items(
                ) if int(summons['violation_precinct']) in precincts][0]
                summons['has_date'] = True
                
                violation_code_definition = HUMANIZED_NAMES_FOR_FISCAL_YEAR_DATABASE_VIOLATIONS.get(summons['violation_code'])
                humanized_description = None
                if violation_code_definition:
                    if isinstance(violation_code_definition, list) and summons['issue_date']:
                        # Some violation codes have applied to multiple violations.
                        for possible_description in violation_code_definition:
                            if (datetime.strptime(possible_description['start_date'], '%Y-%m-%d').date() <
                                datetime.strptime(summons['issue_date'], '%Y-%m-%dT%H:%M:%S.%f').date()):

                                humanized_description = possible_description['description']
                    else:
                        humanized_description = violation_code_definition

                summons['violation'] = humanized_description
                fiscal_year_databases_violations_dict[summons['summons_number']] = summons

        merged_violations = {**open_parking_and_camera_violations_dict,
                             **fiscal_year_databases_violations_dict}

        for key, value in merged_violations.items():
            if key in open_parking_and_camera_violations_dict and key in fiscal_year_databases_violations_dict:
                merged_violations[key] = {**open_parking_and_camera_violations_dict[key], **fiscal_year_databases_violations_dict[key]}

        fines_dict = {}
        for prefix in ['fine', 'interest', 'penalty', 'reduction', 'payment']:
            fines_dict[prefix] = sum(v[f'{prefix}_amount'] for v in merged_violations.values(
            ) if v.get(f'{prefix}_amount'))

        fined = round(sum([fines_dict.get('fine', 0), fines_dict.get(
            'interest', 0), fines_dict.get('penalty', 0)]), 2)
        reduced = round(fines_dict.get('reduction', 0), 2)
        paid = round(fines_dict.get('payment'), 2)
        amount_due = round(fined - reduced - paid, 2)

        fines: FineData = FineData(
            fined=fined,
            reduced=reduced,
            paid=paid,
            outstanding=amount_due)

        tickets: list[Tuple[str, int]] = Counter([v['violation'].title() for v in merged_violations.values(
        ) if v.get('violation')]).most_common()

        years: list[Tuple[str, int]] = Counter([datetime.strptime(v['issue_date'], '%Y-%m-%dT%H:%M:%S.%f').strftime('%Y') if v.get(
            'has_date') else 'No Year Available' for v in merged_violations.values()]).most_common()

        boroughs = Counter([v['borough'].title()
                            for v in merged_violations.values()]).most_common()

        camera_streak_data = {
            'Failure to Stop at Red Light': None,
            'Mixed': None,
            'School Zone Speed Camera Violation': None,
        }

        for violation_type in camera_streak_data:
            if violation_type == 'Failure to Stop at Red Light':
                violation_times = sorted(
                    [datetime.strptime(v['issue_date'], '%Y-%m-%dT%H:%M:%S.%f') for v in merged_violations.values(
                    ) if v.get('violation') == 'Failure to Stop at Red Light'])
            elif violation_type == 'Mixed':
                violation_times = sorted(
                    [datetime.strptime(v['issue_date'], '%Y-%m-%dT%H:%M:%S.%f') for v in merged_violations.values(
                    ) if v.get('violation') and v['violation'] in ['Failure to Stop at Red Light',
                                                           'School Zone Speed Camera Violation']])
            elif violation_type == 'School Zone Speed Camera Violation':
                violation_times = sorted(
                    [datetime.strptime(v['issue_date'], '%Y-%m-%dT%H:%M:%S.%f') for v in merged_violations.values(
                    ) if v.get('violation') == 'School Zone Speed Camera Violation'])

            if violation_times:
                max_streak = 0
                min_streak_date = None
                max_streak_date = None

                for date in violation_times:

                    year_later = date + \
                        (datetime(date.year + 1, 1, 1) - datetime(date.year, 1, 1))

                    year_long_tickets = [
                        comp_date for comp_date in violation_times if date <= comp_date < year_later]
                    this_streak = len(year_long_tickets)

                    if this_streak > max_streak:

                        max_streak = this_streak
                        min_streak_date = year_long_tickets[0]
                        max_streak_date = year_long_tickets[-1]

                camera_streak_data[violation_type] = CameraStreakData(
                    min_streak_date=min_streak_date.strftime('%B %-d, %Y'),
                    max_streak=max_streak,
                    max_streak_date=max_streak_date.strftime('%B %-d, %Y'))

        plate_query = PlateQuery(
            created_at='Tue Dec 31 19:28:12 -0500 2019',
            message_id=random.randint(
                1000000000000000000,
                2000000000000000000),
            message_source='status',
            plate=plate,
            plate_types=plate_type,
            state=state,
            username='******')

        result = OpenDataServiceResponse(
            data=OpenDataServicePlateLookup(
                boroughs=[{'count': v, 'title': k.title()}
                          for k, v in boroughs],
                camera_streak_data=camera_streak_data,
                fines=fines,
                num_violations=len(merged_violations),
                plate=final_plate,
                plate_types=plate_query.plate_types,
                state=plate_query.state,
                violations=[{'count': v, 'title': k.title()}
                            for k, v in tickets],
                years=sorted([{'count': v, 'title': k.title()}
                              for k, v in years], key=lambda k: k['title'])),
            success=True)

        self.assertEqual(self.open_data_service.look_up_vehicle(plate_query),
                         result)
    def _form_plate_lookup_response_parts(
            self,
            borough_data: list[Tuple[str, int]],
            frequency: int,
            fine_data: FineData,
            lookup_source: str,
            plate: str,
            plate_types: list[str],
            state: str,
            unique_identifier: str,
            username: str,
            violations: list[Tuple[str, int]],
            year_data: list[Tuple[str, int]],
            camera_streak_data: dict[str, CameraStreakData] = None,
            previous_lookup: Optional[PlateLookup] = None):

        # response_chunks holds tweet-length-sized parts of the response
        # to be tweeted out or appended into a single direct message.
        response_chunks: list[str] = []
        violations_string: str = ""

        # Get total violations
        total_violations: int = sum([s['count'] for s in violations])
        LOG.debug(f'total_violations: {total_violations}')

        now_in_eastern_time = self.utc.localize(datetime.now())
        time_prefix = now_in_eastern_time.strftime(
            'As of %I:%M:%S %p on %B %-d, %Y:\n\n')

        # Append username to blank string to start to build tweet.
        violations_string += (f'@{username} {time_prefix}' if lookup_source
                              == lookup_sources.LookupSource.STATUS.value else
                              '')

        # Append summary string.
        violations_string += L10N.LOOKUP_SUMMARY_STRING.format(
            L10N.VEHICLE_HASHTAG.format(state, plate),
            L10N.get_plate_types_string(plate_types), frequency,
            L10N.pluralize(int(frequency)))

        # If this vehicle has been queried before...
        if previous_lookup:
            previous_num_violations: int = previous_lookup.num_tickets

            violations_string += self._create_repeat_lookup_string(
                new_violations=(total_violations - previous_num_violations),
                plate=plate,
                previous_lookup=previous_lookup,
                state=state,
                username=username)

        response_chunks.append(violations_string)

        username_prefix = (
            f'@{twitter_constants.HMDNY_TWITTER_HANDLE} {time_prefix}' if
            lookup_source == lookup_sources.LookupSource.STATUS.value else '')

        response_chunks += self._handle_response_part_formation(
            collection=violations,
            continued_format_string=L10N.LOOKUP_TICKETS_STRING_CONTD.format(
                L10N.VEHICLE_HASHTAG.format(state, plate)),
            count='count',
            description='title',
            default_description='No Year Available',
            prefix_format_string=L10N.LOOKUP_TICKETS_STRING.format(
                total_violations),
            result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
            username_prefix=username_prefix)

        if year_data:
            response_chunks += self._handle_response_part_formation(
                collection=year_data,
                continued_format_string=L10N.LOOKUP_YEAR_STRING_CONTD.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                count='count',
                description='title',
                default_description='No Year Available',
                prefix_format_string=L10N.LOOKUP_YEAR_STRING.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
                username_prefix=username_prefix)

        if borough_data:
            response_chunks += self._handle_response_part_formation(
                collection=borough_data,
                continued_format_string=L10N.LOOKUP_BOROUGH_STRING_CONTD.
                format(L10N.VEHICLE_HASHTAG.format(state, plate)),
                count='count',
                description='title',
                default_description='No Borough Available',
                prefix_format_string=L10N.LOOKUP_BOROUGH_STRING.format(
                    L10N.VEHICLE_HASHTAG.format(state, plate)),
                result_format_string=L10N.LOOKUP_RESULTS_DETAIL_STRING,
                username_prefix=username_prefix)

        if fine_data and fine_data.fines_assessed():

            cur_string = (
                f'{username_prefix}'
                f'Known fines for {L10N.VEHICLE_HASHTAG.format(state, plate)}:\n\n'
            )

            max_count_length = len('${:,.2f}'.format(fine_data.max_amount()))
            spaces_needed = (max_count_length * 2) + 1

            for fine_type, amount in fine_data:

                currency_string = '${:,.2f}'.format(amount)
                count_length = len(str(currency_string))

                # e.g., if spaces_needed is 5, and count_length is 2, we need
                # to pad to 3.
                left_justify_amount = spaces_needed - count_length

                # formulate next string part
                next_part = (f"{currency_string.ljust(left_justify_amount)}| "
                             f"{fine_type.replace('_', ' ').title()}\n")

                # determine current string length if necessary
                potential_response_length = len(cur_string + next_part)

                # If violation string so far and new part are less or
                # equal than 280 characters, append to existing tweet string.
                if (potential_response_length <=
                        twitter_constants.MAX_TWITTER_STATUS_LENGTH):
                    cur_string += next_part
                else:
                    response_chunks.append(cur_string)

                    cur_string = "Known fines for #{}_{}, cont'd:\n\n"
                    cur_string += next_part

            # add to container
            response_chunks.append(cur_string)

        for camera_violation_type, threshold in self.CAMERA_THRESHOLDS.items():
            violation_type_data = camera_streak_data[camera_violation_type]

            if violation_type_data:

                if (camera_violation_type == 'Failure to Stop at Red Light'
                        and violation_type_data.max_streak >= threshold):

                    # add to container
                    response_chunks.append(
                        L10N.
                        DANGEROUS_VEHICLE_ABATEMENT_ACT_REPEAT_OFFENDER_STRING.
                        format(username_prefix, violation_type_data.max_streak,
                               'red light', threshold,
                               violation_type_data.min_streak_date,
                               violation_type_data.max_streak_date))

                elif (camera_violation_type
                      == 'School Zone Speed Camera Violation'
                      and violation_type_data.max_streak >= threshold):

                    # add to container
                    response_chunks.append(
                        L10N.
                        DANGEROUS_VEHICLE_ABATEMENT_ACT_REPEAT_OFFENDER_STRING.
                        format(username_prefix, violation_type_data.max_streak,
                               'school zone speed', threshold,
                               violation_type_data.min_streak_date,
                               violation_type_data.max_streak_date))

        unique_link: str = self._get_website_plate_lookup_link(
            unique_identifier)

        website_link_string = f'View more details at {unique_link}.'
        response_chunks.append(website_link_string)

        # Send it back!
        return response_chunks
    def test_print_reckless_driver_retrospective(
            self,
            mocked_tweet_detection_service_tweet_exists,
            mocked_traffic_violations_tweeter,
            mocked_plate_lookup_get_all_in,
            mocked_open_data_service_look_up_vehicle,
            can_link_tweet=True,
            dry_run=False,
            new_camera_violations_string='10 new camera violations',
            red_light_camera_tickets_after_previous_lookup=3,
            red_light_camera_tickets_before_previous_lookup=5,
            speed_camera_tickets_after_previous_lookup=7,
            speed_camera_tickets_before_previous_lookup=15,
            use_dvaa_thresholds: bool = False,
            use_only_visible_tweets: bool = False):

        job = RecklessDriverRetrospectiveJob()

        mocked_tweet_detection_service_tweet_exists.return_value = can_link_tweet

        plate = 'ABCDEFG'
        plate_types = 'COM,PAS'
        state = 'NY'

        now = datetime.now()
        previous_message_id = random.randint(1000000000000000000,
                                             2000000000000000000)
        previous_message_source = 'status'
        previous_num_tickets = 123
        previous_username = '******'

        rdaa_max_streak = (red_light_camera_tickets_before_previous_lookup +
                           speed_camera_tickets_before_previous_lookup)
        rdaa_min_streak_date = datetime(2018, 11, 26, 0, 0,
                                        0).strftime('%B %-d, %Y')
        rdaa_max_streak_date = datetime(2019, 11, 22, 0, 0,
                                        0).strftime('%B %-d, %Y')

        dvaa_red_light_camera_max_streak = red_light_camera_tickets_before_previous_lookup
        dvaa_red_light_camera_min_streak_date = datetime(
            2018, 9, 14, 0, 0, 0).strftime('%B %-d, %Y')
        dvaa_red_light_camera_max_streak_date = datetime(
            2019, 12, 17, 0, 0, 0).strftime('%B %-d, %Y')

        dvaa_speed_camera_max_streak = speed_camera_tickets_before_previous_lookup
        dvaa_speed_camera_min_streak_date = datetime(2018, 11, 26, 0, 0,
                                                     0).strftime('%B %-d, %Y')
        dvaa_speed_camera_max_streak_date = datetime(2019, 11, 22, 0, 0,
                                                     0).strftime('%B %-d, %Y')

        plate_lookup_to_return = PlateLookup(
            bus_lane_camera_violations=2,
            created_at=datetime(2020, 1, 3, 14, 37, 12),
            message_id=previous_message_id,
            message_source=previous_message_source,
            num_tickets=previous_num_tickets,
            plate=plate,
            plate_types=plate_types,
            red_light_camera_violations=
            red_light_camera_tickets_before_previous_lookup,
            speed_camera_violations=speed_camera_tickets_before_previous_lookup,
            state=state,
            username=previous_username)

        below_dvaa_thresholds = (dvaa_red_light_camera_max_streak < 5
                                 and dvaa_speed_camera_max_streak < 15)

        new_camera_violations_exist = (
            red_light_camera_tickets_after_previous_lookup +
            speed_camera_tickets_after_previous_lookup) > 0

        if use_dvaa_thresholds and below_dvaa_thresholds:
            # We don't expect a lookup here.
            expect_response = False
            plate_lookups = []

        elif use_only_visible_tweets and not can_link_tweet:
            # We don't expect a lookup here.
            expect_response = False
            plate_lookups = [plate_lookup_to_return]

        elif not new_camera_violations_exist:
            # We don't expect a lookup here.
            expect_response = False
            plate_lookups = [plate_lookup_to_return]

        elif dry_run:
            # We don't expect a lookup here.
            expect_response = False
            plate_lookups = [plate_lookup_to_return]

        else:
            expect_response = True
            plate_lookups = [plate_lookup_to_return]

        mocked_plate_lookup_get_all_in.return_value = plate_lookups

        open_data_plate_lookup_before_previous_lookup = OpenDataServicePlateLookup(
            boroughs=[],
            camera_streak_data={
                'Failure to Stop at Red Light':
                CameraStreakData(
                    min_streak_date=dvaa_red_light_camera_min_streak_date,
                    max_streak=dvaa_red_light_camera_max_streak,
                    max_streak_date=dvaa_red_light_camera_max_streak_date),
                'Mixed':
                CameraStreakData(min_streak_date=rdaa_min_streak_date,
                                 max_streak=rdaa_max_streak,
                                 max_streak_date=rdaa_max_streak_date),
                'School Zone Speed Camera Violation':
                CameraStreakData(
                    min_streak_date=dvaa_speed_camera_min_streak_date,
                    max_streak=dvaa_speed_camera_max_streak,
                    max_streak_date=dvaa_speed_camera_max_streak_date),
            },
            fines=FineData(),
            num_violations=150,
            plate=plate,
            plate_types=plate_types,
            state=state,
            violations=[{
                'count': red_light_camera_tickets_before_previous_lookup,
                'title': 'Failure To Stop At Red Light'
            }, {
                'count': speed_camera_tickets_before_previous_lookup,
                'title': 'School Zone Speed Camera Violation'
            }],
            years=[])

        open_data_response_before_previous_lookup = OpenDataServiceResponse(
            data=open_data_plate_lookup_before_previous_lookup, success=True)

        open_data_plate_lookup_after_previous_lookup = OpenDataServicePlateLookup(
            boroughs=[],
            camera_streak_data={
                'Failure to Stop at Red Light':
                CameraStreakData(
                    min_streak_date=dvaa_red_light_camera_min_streak_date,
                    max_streak=dvaa_red_light_camera_max_streak,
                    max_streak_date=dvaa_red_light_camera_max_streak_date),
                'Mixed':
                CameraStreakData(min_streak_date=rdaa_min_streak_date,
                                 max_streak=rdaa_max_streak,
                                 max_streak_date=rdaa_max_streak_date),
                'School Zone Speed Camera Violation':
                CameraStreakData(
                    min_streak_date=dvaa_speed_camera_min_streak_date,
                    max_streak=dvaa_speed_camera_max_streak,
                    max_streak_date=dvaa_speed_camera_max_streak_date),
            },
            fines=FineData(),
            num_violations=150,
            plate=plate,
            plate_types=plate_types,
            state=state,
            violations=[{
                'count': red_light_camera_tickets_after_previous_lookup,
                'title': 'Failure To Stop At Red Light'
            }, {
                'count': speed_camera_tickets_after_previous_lookup,
                'title': 'School Zone Speed Camera Violation'
            }],
            years=[])

        open_data_response_after_previous_lookup = OpenDataServiceResponse(
            data=open_data_plate_lookup_after_previous_lookup, success=True)

        mocked_open_data_service_look_up_vehicle.side_effect = [
            open_data_response_before_previous_lookup,
            open_data_response_after_previous_lookup
        ]

        can_link_tweet_string = (
            f' by @BarackObama: '
            f'https://twitter.com/BarackObama/status/{previous_message_id}'
            if can_link_tweet else '')
        red_light_camera_tickets_diff_string = (
            f'{red_light_camera_tickets_after_previous_lookup} | Red Light Camera Violations\n'
            if red_light_camera_tickets_after_previous_lookup else '')

        speed_camera_tickets_diff_string = (
            f'{speed_camera_tickets_after_previous_lookup} | Speed Safety Camera Violations\n'
            if speed_camera_tickets_after_previous_lookup else '')

        summary_string = (
            f'#NY_ABCDEFG was originally queried on January 3, 2020 at 09:37AM'
            f'{can_link_tweet_string}.')

        update_string = (
            f'From November 26, 2018 to November 22, 2019, this vehicle '
            f'received 20 camera violations. Over the past 12 months, this '
            f'vehicle received {new_camera_violations_string}: \n\n'
            f'{red_light_camera_tickets_diff_string}'
            f'{speed_camera_tickets_diff_string}')

        job.run(is_dry_run=dry_run,
                use_only_visible_tweets=use_only_visible_tweets)

        if expect_response:
            mocked_traffic_violations_tweeter().send_status.assert_called_with(
                message_parts=[summary_string, update_string],
                on_error_message=(f'Error printing reckless driver update. '
                                  f'Tagging @bdhowald.'))
        else:
            mocked_traffic_violations_tweeter().send_status.assert_not_called()
    def test_print_reckless_driver_retrospective(self,
                                 mocked_tweet_detection_service_tweet_exists,
                                 mocked_traffic_violations_tweeter,
                                 mocked_plate_lookup_get_all_in,
                                 mocked_plate_lookup_query,
                                 mocked_open_data_service_look_up_vehicle,
                                 can_link_tweet=False,
                                 dry_run=False,
                                 new_camera_violations_string='10 new camera violations',
                                 red_light_camera_tickets_after_previous_lookup=3,
                                 speed_camera_tickets_after_previous_lookup=7,
                                 red_light_camera_tickets_before_previous_lookup=8,
                                 speed_camera_tickets_before_previous_lookup=12):

        job = RecklessDriverRetrospectiveJob()

        # mocked_plate_lookup_query.session.query(
        # ).filter().group_by().all.return_value = [1,2,3]

        mocked_tweet_detection_service_tweet_exists.return_value = can_link_tweet

        plate = 'ABCDEFG'
        plate_types = 'COM,PAS'
        state = 'NY'

        now = datetime.now()
        previous_message_id = random.randint(1000000000000000000, 2000000000000000000)
        previous_message_source = 'status'
        previous_num_tickets = 123
        previous_username = '******'

        max_streak = 20
        min_streak_date = datetime(2018, 11, 26, 0, 0, 0).strftime('%B %-d, %Y')
        max_streak_date = datetime(2019, 11, 22, 0, 0, 0).strftime('%B %-d, %Y')

        plate_lookups = [
            PlateLookup(
                bus_lane_camera_violations=2,
                created_at=datetime(2020, 1, 3, 14, 37, 12),
                message_id=previous_message_id,
                message_source=previous_message_source,
                num_tickets=previous_num_tickets,
                plate=plate,
                plate_types=plate_types,
                red_light_camera_violations=red_light_camera_tickets_before_previous_lookup,
                speed_camera_violations=speed_camera_tickets_before_previous_lookup,
                state=state,
                username=previous_username)]

        mocked_plate_lookup_get_all_in.return_value = plate_lookups

        open_data_plate_lookup_before_previous_lookup = OpenDataServicePlateLookup(
            boroughs=[],
            camera_streak_data=CameraStreakData(
                min_streak_date=min_streak_date,
                max_streak=max_streak,
                max_streak_date=max_streak_date),
            fines=FineData(),
            num_violations=150,
            plate=plate,
            plate_types=plate_types,
            state=state,
            violations=[
                {'count': red_light_camera_tickets_before_previous_lookup,
                 'title': 'Failure To Stop At Red Light'},
                {'count': speed_camera_tickets_before_previous_lookup, 'title':
                 'School Zone Speed Camera Violation'}],
            years=[])

        open_data_response_before_previous_lookup = OpenDataServiceResponse(
            data=open_data_plate_lookup_before_previous_lookup,
            success=True)


        open_data_plate_lookup_after_previous_lookup = OpenDataServicePlateLookup(
            boroughs=[],
            camera_streak_data=CameraStreakData(
                min_streak_date=min_streak_date,
                max_streak=max_streak,
                max_streak_date=max_streak_date),
            fines=FineData(),
            num_violations=150,
            plate=plate,
            plate_types=plate_types,
            state=state,
            violations=[
                {'count': red_light_camera_tickets_after_previous_lookup,
                 'title': 'Failure To Stop At Red Light'},
                {'count': speed_camera_tickets_after_previous_lookup, 'title':
                 'School Zone Speed Camera Violation'}],
            years=[])

        open_data_response_after_previous_lookup = OpenDataServiceResponse(
            data=open_data_plate_lookup_after_previous_lookup,
            success=True)

        mocked_open_data_service_look_up_vehicle.side_effect = [
            open_data_response_before_previous_lookup, open_data_response_after_previous_lookup]

        can_link_tweet_string = (
          f' by @BarackObama: '
          f'https://twitter.com/BarackObama/status/{previous_message_id}'
          if can_link_tweet else '')
        red_light_camera_tickets_diff_string = (
            f'{red_light_camera_tickets_after_previous_lookup} | Red Light Camera Violations\n'
            if red_light_camera_tickets_after_previous_lookup else '')

        speed_camera_tickets_diff_string = (
            f'{speed_camera_tickets_after_previous_lookup} | Speed Safety Camera Violations\n'
            if speed_camera_tickets_after_previous_lookup else '')

        summary_string = (
            f'#NY_ABCDEFG was originally queried on January 3, 2020 at 09:37AM'
            f'{can_link_tweet_string}.')

        update_string = (
            f'From November 26, 2018 to November 22, 2019, this vehicle '
            f'received 20 camera violations. Over the past 12 months, this '
            f'vehicle received {new_camera_violations_string}: \n\n'
            f'{red_light_camera_tickets_diff_string}'
            f'{speed_camera_tickets_diff_string}')

        reckless_string = (
            f'Thank you to @bradlander for making the Dangerous Driver '
            f'Abatement Act a reality.')

        job_should_be_run = (red_light_camera_tickets_after_previous_lookup +
            speed_camera_tickets_after_previous_lookup) > 0

        job.run(is_dry_run=dry_run)

        if not dry_run and job_should_be_run:
            mocked_traffic_violations_tweeter().send_status.assert_called_with(
                message_parts=[summary_string, update_string, reckless_string],
                on_error_message=(
                    f'Error printing reckless driver update. '
                    f'Tagging @bdhowald.'))