Exemplo n.º 1
0
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")
Exemplo n.º 2
0
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)
Exemplo n.º 3
0
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()
Exemplo n.º 4
0
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()
Exemplo n.º 5
0
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()
Exemplo n.º 6
0
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
Exemplo n.º 7
0
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
Exemplo n.º 8
0
 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 = []
Exemplo n.º 9
0
    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
Exemplo n.º 10
0
    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))
Exemplo n.º 11
0
    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
Exemplo n.º 12
0
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)