def _get_opening_dates(site: dict) -> location.OpenDate: opens = _parse_datetime(site["dateFrom"]) closes = _parse_datetime(site["dateTo"]) if opens: opens = opens.date().isoformat() if closes: closes = closes.date().isoformat() return [location.OpenDate(opens=opens, closes=closes)]
def _get_opening_dates(site: dict) -> Optional[List[schema.OpenDate]]: start_date = site["attributes"]["StartDate"] end_date = site["attributes"]["EndDate"] if start_date or end_date: return [schema.OpenDate(opens=start_date, closes=end_date)] return None
def _make_opening_dates_contiguous( dates: List[datetime]) -> List[schema.OpenDate]: """ Converts an list of `dates` into an equivalent list of inclusive `OpenDate` ranges. For example, `[2021-06-01, 2021-06-02, 2021-06-04]` becomes `[{opens: 2021-06-01, closes: 2021-06-02}, {opens: 2021-06-04, closes: 2021-06-04}]`. """ dates.sort() ranges: List[schema.OpenDate] = [] current_opens: Optional[datetime] = None current_closes: Optional[datetime] = None # Invariants: # - current_range.{opens,closes} are None at the beginning, and non-None otherwise. # - At iteration i (date = dates[i]): # dates[0..i-1] = concat([r.opens..r.closes] for r in ranges) + [current_opens..current_closes] # Here [ ] denote inclusive ranges on both ends. for date in dates: if not (current_opens and current_closes): # First date seen. Start with a singleton range. current_opens = date current_closes = date else: # Compute the difference in days between this new date and the end date of the current range. delta = (date - current_closes).days if delta == 0: # Same date seen twice in succession. # Unlikely, but handle it anyway by changing nothing. logger.debug("Encountered same date twice in succession: %s", date) elif delta == 1: # Extend the current range by one day to include this date. current_closes = date else: # There is a gap of at least 1 day between the current range and this date. # Save the current range. ranges.append( schema.OpenDate(opens=current_opens, closes=current_closes)) # And start a new one. current_opens = date current_closes = date # Include the last range. ranges.append(schema.OpenDate(opens=current_opens, closes=current_closes)) return ranges
def _get_opening_dates(site: dict): if site["Start Date"] == "" and site["End Date"]: return None return [ schema.OpenDate( opens=_normalize_date(site["Start Date"]), closes=_normalize_date(site["End Date"]), ) ]
def _get_opening_dates(site: dict) -> List[schema.OpenDate]: date = site["date"] date_split = date.split("/") return [ schema.OpenDate( opens=f"{date_split[2]}-{date_split[0]}-{date_split[1]}", closes=f"{date_split[2]}-{date_split[0]}-{date_split[1]}", ) ]
def _get_opening_dates(site: dict) -> Optional[List[schema.OpenDate]]: start_date = site["attributes"].get("opendate") if start_date: start_date = datetime.datetime.fromtimestamp(start_date / 1000) if start_date: return [schema.OpenDate(opens=start_date, closes=None)] else: return None
def _get_opening_dates(site: dict) -> Optional[List[schema.OpenDate]]: opens = None closes = None if site["attributes"]["begindate"] is not None: opens = (datetime.datetime.fromtimestamp( site["attributes"]["begindate"] // 1000).date().isoformat()) if site["attributes"]["enddate"] is not None: closes = (datetime.datetime.fromtimestamp( site["attributes"]["enddate"] // 1000).date().isoformat()) if opens is None and closes is None: return None return [schema.OpenDate( opens=opens, closes=closes, )]
def test_opening_days(): assert location.OpenDate( opens="2021-04-01", closes="2021-04-01", ) assert location.OpenDate(opens="2021-04-01") assert location.OpenDate(closes="2021-04-01", ) with pytest.raises(pydantic.error_wrappers.ValidationError): location.OpenDate(closes="2021-04-01T04:04:04", ) with pytest.raises(pydantic.error_wrappers.ValidationError): location.OpenDate(opens="tomorrow", ) with pytest.raises(pydantic.error_wrappers.ValidationError): location.OpenDate( opens="2021-06-01", closes="2021-01-01", )
def _get_opening_dates(site: dict) -> Optional[List[schema.OpenDate]]: """ "May 3 - May 6 and May 10 - May 13 \n9am-1pm \nMay 24 - May 27 \n2pm - 7pm" "May 3-May 6 and May 10-May13 \n9am-1pm \nMay 24-May 27 \n2pm-7pm" "May 5-May 8: 9am-1pm \nMay 12-15, 19-22, 26-29: 2pm-7pm" "May 3: 9am-1pm \nMay 6-8, 10, 13-15, 17, 20-22, 24: 2pm-7pm \nMay 27-29: 9am-1pm" "May 5, 6, 12, 14, 19, 20 \n9am-2pm" """ opening_dates = [] days_hours = site["Normal Days / Hours"].lower().replace("and", ", ") # short-circuit if it's an entry with days, not dates if not re.search(MONTHS_PATTERN, days_hours): return None # split on the "9am-1pm"s (leading ":" is optional, giving us just the # date entries entries = re.split( fr"(?::\s*)?(?:{HOURS_PATTERN})\s*(?:{AM_PM_PATTERN})\s*-\s*(?:{HOURS_PATTERN})\s*(?:{AM_PM_PATTERN})", days_hours, ) for entry in entries: entry_match = re.search(MONTH_DATE_TO_MONTH_DATE_PATTERN, entry) if entry_match: for start_date, end_date in re.findall( MONTH_DATE_TO_MONTH_DATE_PATTERN, entry): # "May 5-May 8" start_date = dateparser.parse(start_date).date().isoformat() end_date = dateparser.parse(end_date).date().isoformat() opening_dates.append( schema.OpenDate(opens=start_date, closes=end_date)) continue entry_match = re.search( fr"(?P<month>({MONTHS_PATTERN}))\s+(?P<dates>({DATE_SINGLE_OR_RANGE_PATTERN})(\s*,\s*({DATE_SINGLE_OR_RANGE_PATTERN}))*)", entry, ) if entry_match: # "May 6-8, 10, 13-15, 17, 20-22, 24" # "May 11" month = entry_match.group("month") pieces = entry_match.group("dates") for piece in re.split(r"\s*,\s*", pieces): if "-" in piece: start, end = re.split(r"\s*-\s*", piece) start_date = dateparser.parse( f"{month} {start}").date().isoformat() end_date = dateparser.parse( f"{month} {end}").date().isoformat() opening_dates.append( schema.OpenDate(opens=start_date, closes=end_date)) else: date = dateparser.parse( f"{month} {piece}").date().isoformat() opening_dates.append( schema.OpenDate(opens=date, closes=date)) continue if entry: logger.info(f"Unparseable opening_dates: {entry}") return opening_dates or None
def full_location(): return location.NormalizedLocation( id="source:dad13365-2202-4dea-9b37-535288b524fe", name="Rite Aid Pharmacy 5952", address=location.Address( street1="1991 Mountain Boulevard", city="Oakland", state="CA", zip="94611", ), location=location.LatLng( latitude=37.8273167, longitude=-122.2105179, ), contact=[ location.Contact( contact_type=location.ContactType.BOOKING, phone="(916) 445-2841", ), location.Contact( contact_type=location.ContactType.GENERAL, phone="(510) 339-2215", ), ], languages=["en"], opening_dates=[ location.OpenDate( opens="2021-04-01", closes="2021-04-01", ), ], opening_hours=[ location.OpenHour( day=location.DayOfWeek.MONDAY, opens="08:00", closes="14:00", ), ], availability=location.Availability( drop_in=False, appointments=True, ), inventory=[ location.Vaccine( vaccine=location.VaccineType.MODERNA, supply_level=location.VaccineSupply.IN_STOCK, ), ], access=location.Access( walk=True, drive=False, wheelchair=location.WheelchairAccessLevel.PARTIAL, ), parent_organization=location.Organization( id=location.VaccineProvider.RITE_AID, name="Rite Aid Pharmacy", ), links=[ location.Link( authority=location.LocationAuthority.GOOGLE_PLACES, id="ChIJA0MOOYWHj4ARW8M-vrC9yGA", ), ], notes=["long note goes here"], active=True, source=location.Source( source="source", id="dad13365-2202-4dea-9b37-535288b524fe", fetched_from_uri="https://example.org", fetched_at="2020-04-04T04:04:04.4444", published_at="2020-04-04T04:04:04.4444", data={"id": "dad13365-2202-4dea-9b37-535288b524fe"}, ), )
def test_valid_location(): # Minimal record assert location.NormalizedLocation( id="source:id", source=location.Source( source="source", id="id", data={"id": "id"}, ), ) # Full record with str enums full_loc = location.NormalizedLocation( id="source:id", name="name", address=location.Address( street1="1991 Mountain Boulevard", street2="#1", city="Oakland", state="CA", zip="94611", ), location=location.LatLng( latitude=37.8273167, longitude=-122.2105179, ), contact=[ location.Contact( contact_type="booking", phone="(916) 445-2841", ) ], languages=["en"], opening_dates=[ location.OpenDate( opens="2021-04-01", closes="2021-04-01", ), ], opening_hours=[ location.OpenHour( day="monday", opens="08:00", closes="14:00", ), ], availability=location.Availability( drop_in=False, appointments=True, ), inventory=[ location.Vaccine( vaccine="moderna", supply_level="in_stock", ), ], access=location.Access( walk=True, drive=False, wheelchair="partial", ), parent_organization=location.Organization( id="rite_aid", name="Rite Aid", ), links=[ location.Link( authority="google_places", id="abc123", ), ], notes=["note"], active=True, source=location.Source( source="source", id="id", fetched_from_uri="https://example.org", fetched_at="2020-04-04T04:04:04.4444", published_at="2020-04-04T04:04:04.4444", data={"id": "id"}, ), ) assert full_loc # Verify dict serde full_loc_dict = full_loc.dict() assert full_loc_dict parsed_full_loc = location.NormalizedLocation.parse_obj(full_loc_dict) assert parsed_full_loc assert parsed_full_loc == full_loc # Verify json serde full_loc_json = full_loc.json() assert full_loc_json parsed_full_loc = location.NormalizedLocation.parse_raw(full_loc_json) assert parsed_full_loc assert parsed_full_loc == full_loc # Verify dict->json serde full_loc_json_dumps = json.dumps(full_loc_dict) assert full_loc_json_dumps assert full_loc_json_dumps == full_loc_json parsed_full_loc = location.NormalizedLocation.parse_raw( full_loc_json_dumps) assert parsed_full_loc assert parsed_full_loc == full_loc