def get_opening_hours_osm_format_week_description( opening_hours_osm_format_string: str, ) -> list: """ Transform `opening_hours_osm_format` into a list of readable descriptions per day. For example, if `opening_hours_osm_format` contains the string "Mo-Fr 08:00-20:00", this method returns the following output: [ 'Lundi : 08:00 – 20:00', 'Mardi : 08:00 – 20:00', 'Mercredi : 08:00 – 20:00', 'Jeudi : 08:00 – 20:00', 'Vendredi : 08:00 – 20:00', 'Samedi : fermé' 'Dimanche : fermé' ] TODO: Store as model field ? """ if not opening_hours_osm_format_string: return [] oh = hoh.OHParser(opening_hours_osm_format_string, locale="fr") return oh.plaintext_week_description().split("\n")
def parse_time_block(cls, es_poi, lang, raw): if not raw: return None poi_coord = get_coord(es_poi) poi_lat = poi_coord[0] poi_lon = poi_coord[1] poi_tzname = tz.tzNameAt(poi_lat, poi_lon, forceTZ=True) poi_tz = get_tz(poi_tzname, poi_lat, poi_lon) if poi_tz is None: logger.info("No timezone found for poi %s", es_poi.get('id')) return None hoh_args = {} if any(k in raw for k in ['sunset', 'sunrise', 'dawn', 'dusk']): # Optimization: use astral location only when necessary hoh_args['location'] = (poi_lat, poi_lon, poi_tzname, 24) try: oh = hoh.OHParser(raw, **hoh_args) except (HOHError, LarkError): logger.info("Failed to parse happy_hours field, id:'%s' raw:'%s'", es_poi.get_id(), raw, exc_info=True) return None poi_dt = UTC.localize(datetime.utcnow()).astimezone(poi_tz) if oh.is_open(poi_dt.replace(tzinfo=None)): status = cls.STATUSES[0] else: status = cls.STATUSES[1] if cls.BLOCK_TYPE == OpeningHourBlock.BLOCK_TYPE and (raw == '24/7' or oh.is_24_7): return cls.init_class(status, None, None, oh, poi_dt, raw) # The current version of the hoh lib doesn't allow to use the next_change() function # with an offset aware datetime. # This is why we replace the timezone info until this problem is fixed in the library. try: nt = oh.next_change(dt=poi_dt.replace(tzinfo=None)) except HOHError: logger.info("HOHError: Failed to compute next transition for poi %s", es_poi.get('id'), exc_info=True) return None # Then we localize the next_change transition datetime in the local POI timezone. next_transition = poi_tz.localize(nt.replace(tzinfo=None)) next_transition = round_dt_to_minute(next_transition) next_transition_datetime = next_transition.isoformat() delta = next_transition - poi_dt time_before_next = int(delta.total_seconds()) return cls.init_class(status, next_transition_datetime, time_before_next, oh, poi_dt, raw)
def get_opening_hours_osm_format_is_open( opening_hours_osm_format_string: str) -> bool: """ Return `True` if the `place` is currently open, or `False` otherwise. """ if not opening_hours_osm_format_string: return False oh = hoh.OHParser(opening_hours_osm_format_string, locale="fr") return oh.is_open()
def _parse_opening_hours_resp(open_hours_text: str) -> Dict[str, str]: open_hours_model = {} try: oh = hoh.OHParser(open_hours_text) for day in working_days + weekend_days: open_time_for_day = oh.render().periods_of_day( week_days_datetime[day]).description open_hours_model[day] = open_time_for_day return open_hours_model except AttributeError: return _get_mocked_opening_hours()
def get_opening_hours_osm_format_description( opening_hours_osm_format_string: str, ) -> list: """ Transform opening_hours_osm_format into a readable description 'Mo-Fr 08:00-20:00' --> ['Du lundi au vendredi : 08:00 – 20:00.'] TODO: Store as model field ? """ if not opening_hours_osm_format_string: return [] oh = hoh.OHParser(opening_hours_osm_format_string, locale="fr") return oh.description()
def _sanitize_opening_hours_with_hoh(opening_hours: str): """ https://github.com/rezemika/humanized_opening_hours#basic-methods Cleans up the opening_hours string Must be close to the OSM format Returns an Exception if it fails to parse/sanitize the string ' mo-su 09:30-20h;jan off' --> 'Mo-Su 09:30-20:00; Jan off' 'Tu 14h-16h' --> 'Tu 14:00-16:00' """ sanitized_opening_hours = hoh.sanitize(opening_hours.strip()) hoh.OHParser(sanitized_opening_hours, locale="fr") # fails if given a wrong format return sanitized_opening_hours
def get_opening_hours_osm_format_today( opening_hours_osm_format_string: str) -> list: """ Get the opening times of the current day. For example, if `opening_hours_osm_format` contains the string "Mo-Fr 8:00-20:00", this method returns the following output: [ { 'beginning': datetime.datetime(2020, 4, 8, 8, 0), 'end': datetime.datetime(2020, 4, 8, 20, 0), 'status': True, 'timespan': <TimeSpan from ('normal', datetime.time(8, 0)) to ('normal', datetime.time(20, 0))> # noqa } ] Usage: loop on results, then loop on timespan """ if not opening_hours_osm_format_string: return [] oh = hoh.OHParser(opening_hours_osm_format_string, locale="fr") return oh.get_day().timespans
def __init__(self, row=None, name=None, location=None, keywords=None): if row: self.id = row['id'] self.name = row['name'] self.key = row['str_id'] self.hours_src = row['hours'] self.hours = hoh.OHParser(row['hours']) if row['hours'] else None self.links = json.loads(row['links'] or '[]') self.photo_out = row['photo_out'] self.photo_in = row['photo_in'] self.location = Location(lon=row['lon'], lat=row['lat']) self.description = row['description'] self.comment = row['comment'] self.house = row['house'] self.house_name = row['h_address'] if 'h_address' in row.keys( ) else None self.address_part = row['address'] self.keywords = row['keywords'] if not row['phones']: self.phones = [] else: self.phones = [p.strip() for p in row['phones'].split(';')] self.has_wifi = None if row['has_wifi'] is None else row[ 'has_wifi'] == 1 self.accepts_cards = None if row['accepts_cards'] is None else row[ 'accepts_cards'] == 1 self.tag = row['tag'] self.floor = row['flor'] self.needs_check = row['needs_check'] == 1 self.delete_reason = row['delete_reason'] else: self.id = None self.name = name self.location = location self.keywords = keywords self.phones = [] self.links = []
def cost_fun(u, v, d): """Cost function that evaluates every edge, returning either a nonnegative cost or None. Returning a value of None implies an infinite cost, i.e. that edge will be excluded from any paths. :param u: incoming node ID :type u: int :param v: ougoing node ID :type v: int :param d: The edge to evaluate. :type d: dict :returns: Cost of traversing the edge :rtype: float or None """ time = 0 speed = base_speed layer = d["_layer"] length = d["length"] if layer == "sidewalks": incline = d["incline"] # Decrease speed based on incline if length > 3: if incline > uphill: return None if incline < -downhill: return None if incline > INCLINE_IDEAL: speed = tobler(incline, k=k_up, m=INCLINE_IDEAL, base=base_speed) else: speed = tobler(incline, k=k_down, m=INCLINE_IDEAL, base=base_speed) elif layer == "crossings": # Add delay for crossing street time += 30 # Avoid raised curbs based on user setting if avoid_curbs and not d["curbramps"]: return None elif layer == "elevator_paths": opening_hours = d["opening_hours"] # Add delay for using the elevator time += 45 # See if the elevator has limited hours try: oh = hoh.OHParser(opening_hours) if not oh.is_open(date): return None except KeyError: # 'opening_hours' isn't on this elevator path pass except ValueError: # 'opening_hours' is None (better option for checking?) pass except: # Something else went wrong. TODO: give a useful message back? return None else: # This shouldn't happen. Raise error? return None # Initial time estimate (in seconds) - based on speed time = length / speed # Return time estimate - this is currently the cost return time
def from_es(cls, es_poi, lang): raw = es_poi.get('properties', {}).get('opening_hours') if raw is None: return None poi_coord = get_coord(es_poi) poi_lat = poi_coord[0] poi_lon = poi_coord[1] is247 = raw == '24/7' poi_tzname = tz.tzNameAt(poi_lat, poi_lon, forceTZ=True) poi_tz = get_tz(poi_tzname, poi_lat, poi_lon) poi_location = (poi_lat, poi_lon, poi_tzname, 24) if poi_tz is None: logger.info("No timezone found for poi %s", es_poi.get('id')) return None try: oh = hoh.OHParser(raw, location=poi_location) except HOHError: logger.info("Failed to parse OSM opening_hour field", exc_info=True) return None poi_dt = UTC.localize(datetime.utcnow()).astimezone(poi_tz) if oh.is_open(poi_dt.replace(tzinfo=None)): status = 'open' else: status = 'closed' if is247: return cls(status=status, next_transition_datetime=None, seconds_before_next_transition=None, is_24_7=is247, raw=oh.field, days=cls.get_days(oh, dt=poi_dt)) # The current version of the hoh lib doesn't allow to use the next_change() function # with an offset aware datetime. # This is why we replace the timezone info until this problem is fixed in the library. try: nt = oh.next_change(dt=poi_dt.replace(tzinfo=None)) except HOHError: logger.info( "HOHError: Failed to compute next transition for poi %s", es_poi.get('id'), exc_info=True) return None # Then we localize the next_change transition datetime in the local POI timezone. next_transition = poi_tz.localize(nt.replace(tzinfo=None)) next_transition = cls.round_dt_to_minute(next_transition) next_transition_datetime = next_transition.isoformat() delta = next_transition - poi_dt time_before_next = int(delta.total_seconds()) return cls(status=status, next_transition_datetime=next_transition_datetime, seconds_before_next_transition=time_before_next, is_24_7=is247, raw=oh.field, days=cls.get_days(oh, dt=poi_dt))
def cost_fun(u, v, d): """Cost function that evaluates every edge, returning either a nonnegative cost or None. Returning a value of None implies an infinite cost, i.e. that edge will be excluded from any paths. :param u: incoming node ID :type u: int :param v: ougoing node ID :type v: int :param d: The edge to evaluate. :type d: dict :returns: Cost of traversing the edge :rtype: float or None """ time = 0 speed = base_speed length = d["length"] subclass = d["subclass"] if subclass == "footway": if "footway" in d: if d["footway"] == "sidewalk": # FIXME: this data should be float to begin with incline = float(d["incline"]) # Decrease speed based on incline if length > 3: if incline > uphill: return None if incline < -downhill: return None if incline > INCLINE_IDEAL: speed = tobler(incline, k=k_up, m=INCLINE_IDEAL, base=base_speed) else: speed = tobler(incline, k=k_down, m=INCLINE_IDEAL, base=base_speed) elif d["footway"] == "crossing": if avoidCurbs: if "curbramps" in d: if not d["curbramps"]: return None else: # TODO: Make this user-configurable - we assume no # curb ramps by default now return None # Add delay for crossing street # TODO: tune this based on street type crossed and/or markings. time += 30 elif d["elevator"]: opening_hours = d['opening_hours'] # Add delay for using the elevator time += 45 # See if the elevator has limited hours try: oh = hoh.OHParser(opening_hours) if not oh.is_open(date): return None except KeyError: # 'opening_hours' isn't on this elevator path pass except ValueError: # 'opening_hours' is None (better option for checking?) pass except: # Something else went wrong. TODO: give a useful message back? return None else: # Unknown path type return None # Initial time estimate (in seconds) - based on speed time += length / speed # Return time estimate - this is currently the cost return time
async def store_attr(message: types.Message, state: FSMContext): data = await state.get_data() poi = data['poi'] attr = data['attr'] value = message.text.strip() if attr == 'name': if value == '-': await message.answer(tr(('editor', 'empty_name')), reply_markup=cancel_attr_kbd()) return poi.name = value elif attr == 'desc': poi.description = None if value == '-' else value elif attr == 'comment': poi.comment = None if value == '-' else value elif attr == 'floor': poi.floor = None if value == '-' else value elif attr == 'tag': if value == '-': poi.tag = None else: parts = [ p.strip() for p in re.split(r'[ =]+', value.lower().replace('-', '_')) ] if len(parts) != 2 or not re.match(r'^[a-z]+$', parts[0]): await message.answer(tr(('editor', 'tag_format'), value), reply_markup=cancel_attr_kbd()) return poi.tag = '='.join(parts) elif attr == 'keywords': new_kw = split_tokens(value) if new_kw: old_kw = [] if not poi.keywords else poi.keywords.split() poi.keywords = ' '.join(old_kw + new_kw) elif attr == 'address': poi.address_part = None if value == '-' else value elif attr == 'location': loc = parse_location(message) if not loc: await message.answer(tr(('new_poi', 'no_location')), reply_markup=edit_loc_kbd(poi)) return if not valid_location(loc): await message.answer(tr(('new_poi', 'location_out')), reply_markup=edit_loc_kbd(poi)) return poi.location = loc elif attr == 'hours': if value == '-': poi.hours = None poi.hours_src = None else: try: hours = parse_hours(value) except ValueError as e: await message.answer(tr(('editor', 'hours_format'), e), reply_markup=cancel_attr_kbd()) return poi.hours_src = hours poi.hours = hoh.OHParser(hours) elif attr == 'phones': if not value or value == '-': poi.phones = [] else: poi.phones = [p.strip() for p in re.split(r'[;,]', value)] elif attr == 'links': if value: parts = parse_link(value) if parts: if len(parts) == 1: poi.links = [l for l in poi.links if l[0] != parts[0]] else: found = False for i, l in enumerate(poi.links): if l[0] == parts[0]: found = True l[1] = parts[1] if not found: poi.links.append(parts) elif attr == 'delete': await db.delete_poi(message.from_user.id, poi, value) await state.finish() user = await get_user(message.from_user) if user.review: kbd = types.InlineKeyboardMarkup().add( types.InlineKeyboardButton('🗒️ ' + tr(('review', 'continue')), callback_data='continue_review')) else: kbd = get_buttons() await message.answer(tr(('editor', 'deleted')), reply_markup=kbd) await broadcast_str( tr(('editor', 'just_deleted'), id=poi.id, reason=value), message.from_user.id) return else: await message.answer(tr(('editor', 'wrong_attr'), attr)) await delete_msg(message, state) await state.set_data({'poi': poi}) await EditState.confirm.set() await print_edit_options(message.from_user, state)