def parse_general_intent(args: json) -> WeatherRequest:
    """
    Parses general rhasspy weather intents.

    Args:
        args: the rhasspy intent message

    Returns: WeatherRequest object

    """
    config = get_config()
    today = datetime.datetime.now(config.timezone).date()

    # define default request
    new_request = WeatherRequest(DateType.FIXED, Grain.DAY, today, ForecastType.FULL)

    if hasattr(args, "day") and args.day is not None:
        new_request.request_date, new_request.date_specified = parse_date(args.day, config.locale)

    if hasattr(args, "time") and args.time is not None:
        time, str_time = parse_time(args.time, config.locale)
        new_request.set_time(time, str_time)

    if hasattr(args, "location") and args.location is not None:
        new_request.location = parse_location(args.location, config.locale)

    return new_request
Example #2
0
def weekday_to_date(weekday: str, next_week: bool = False) -> datetime.date:
    """
    Takes a string containing a valid weekday (in weekday_names of locale) and returns the date based on today.

    next_week is False (default):
    If today is Wednesday and weekday is "Thursday" it will return the date of tomorrow.
    If today is Wednesday and weekday is "Wednesday" it will return the date of next Wednesday.
    If today is Wednesday and weekday is "Tuesday" it will return the date of next Tuesday.

    next_week is True:
    If today is Wednesday and weekday is "Thursday" it will return the date Thursday next week.
    If today is Wednesday and weekday is "Wednesday" it will return the date of next Wednesday.
    If today is Wednesday and weekday is "Tuesday" it will return the date of next Tuesday.

    Args:
        weekday: string containing the weekday to parse
        next_week: (optional) boolean controlling of a date of the next week will be enforced, default is False

    Returns: the date of the requested weekday

    """
    config = get_config()
    today = datetime.datetime.now(config.timezone).date()
    weekdays_lowercase = [x.lower() for x in config.locale.weekday_names]
    weekday_number = weekdays_lowercase.index(weekday.lower())
    offset = weekday_number - today.weekday()
    if next_week or today.weekday() >= weekday_number:
        offset = offset + 7
    return today + datetime.timedelta(offset)
Example #3
0
def answer(weather_input,
           output,
           config_path: str = None) -> Union[WeatherReport, WeatherError]:
    """
    Function that combines information into the form specified in config and outputs them to where is should go

    Args:
        weather_input: anything that a parser exists for
        output: either a WeatherReport or a WeatherError that contains information
        config_path: optional path to a config file

    Returns:
        output, unless one of the selected outputs has a specified return value. If there is one, it will return that instead

    """
    if config_path is not None and cf.config_path is not config_path:
        cf.set_config_path(config_path)

    config = cf.get_config()
    log.info("Answering")
    return_value = output
    for output_item in config.output:
        try:
            filled_template = fill_template(weather_input, output,
                                            output_item.get_template())
            return_value = output_item.output_response(filled_template)
        except (WeatherError, ConfigError) as e:
            log.error(
                f"Can't output response on {output_item.__name__}: {e.description}"
            )

    return return_value
Example #4
0
def named_day_to_date(named_day: str) -> datetime.date:
    """
    Parses a string containing a named day to the date.

    Args:
        named_day: string containing a valid named day (locale.named_days and locale.named_days_synonyms)

    Returns: the date of the named_day
    """
    locale = get_config().locale
    named_days_lowercase = [x.lower() for x in locale.named_days]
    named_days_synonyms_lowercase = [x.lower() for x in locale.named_days_synonyms]
    value = None
    if named_day.lower() in named_days_synonyms_lowercase:
        index = named_days_synonyms_lowercase.index(named_day.lower())
        name = list(locale.named_days_synonyms.keys())[index]
        value = locale.named_days[locale.named_days_synonyms[name]]
    elif named_day.lower() in named_days_lowercase:
        index = named_days_lowercase.index(named_day.lower())
        value = list(locale.named_days.values())[index]
    if isinstance(value, Tuple):
        return get_date_with_year(value[0], value[1])
    elif isinstance(value, int):
        return datetime.date.today() + datetime.timedelta(value)
    else:
        log.error("Invalid datatype specified in locale.named_days or locale.named_days_synonyms")
        raise WeatherError(ErrorCode.DATE_ERROR)
def parse_general_intent(intent_message: dict) -> WeatherRequest:
    """
    Parses general rhasspy weather intents.

    Args:
        intent_message: the rhasspy intent message

    Returns: WeatherRequest object

    """
    config = get_config()
    today = datetime.datetime.now(config.timezone).date()

    # define default request
    new_request = WeatherRequest(DateType.FIXED, Grain.DAY, today,
                                 ForecastType.FULL)

    slots = intent_message["slots"]

    if slot_names["day"] in slots and slots[slot_names["day"]] != "":
        new_request.request_date, new_request.date_specified = parse_date(
            slots[slot_names["day"]], config.locale)

    if slot_names["time"] in slots and slots[slot_names["time"]] != "":
        time, str_time = parse_time(slots[slot_names["time"]], config.locale)
        new_request.set_time(time, str_time)

    if slot_names["location"] in slots and slots[slot_names["location"]] != "":
        new_request.location = parse_location(slots[slot_names["location"]],
                                              config.locale)

    return new_request
Example #6
0
def named_time_to_time(named_time: str) -> Union[datetime.time, Tuple[datetime.time, datetime.time]]:
    """
    Parsed a string containing a named time into the time.

    Args:
        named_time: a valid named time(locale.named_times or locale.named_times_synonyms)

    Returns: either a time or a tuple containing start and end time of an interval
    """
    locale = get_config().locale
    named_times_lowercase = [x.lower() for x in locale.named_times.keys()]
    named_times_synonyms_lowercase = [x.lower() for x in locale.named_times_synonyms.keys()]
    value = None
    if named_time.lower() in named_times_synonyms_lowercase:
        index = named_times_synonyms_lowercase.index(named_time.lower())
        name = list(locale.named_times_synonyms.keys())[index]
        value = locale.named_times[locale.named_times_synonyms[name]]
    elif named_time.lower() in named_times_lowercase:
        index = named_times_lowercase.index(named_time.lower())
        value = list(locale.named_times.values())[index]
    if isinstance(value, datetime.time) or isinstance(value, tuple):
        return value
    else:
        log.error("Invalid time specified in locale.named_times or locale.named_times_synonyms")
        raise WeatherError(ErrorCode.TIME_ERROR, "Invalid time specified in locale.named_times or locale.named_times_synonyms")
Example #7
0
    def __init__(self, date_type, grain, request_date, forecast_type):
        """
        Parameters:
        date_type : DateType
        grain : Grain
        request_date : datetime.date
        forecast_type : ForecastType
        """

        config = get_config()

        self.__location = config.location
        self.date_type = date_type
        self.grain = grain
        self.request_date = request_date
        self.requested = ""
        self.start_time = None
        self.end_time = None
        self.time_specified = ""
        self.date_specified = ""
        self.location_specified = False
        self.forecast_type = forecast_type
        self.detail = config.detail
        self.__timezone = config.timezone
        self.__locale = config.locale
        self.times = []

        # weather apis don't have weather for the past, so no no need checking
        if self.request_date < datetime.datetime.now(self.__timezone).date():
            raise WeatherError(ErrorCode.PAST_WEATHER_ERROR)
Example #8
0
def test_date_string_to_date(mock_config_detail_true):
    config = get_config()
    result = date_string_to_date("31 " + config.locale.month_names[4])
    assert result.day == 31
    assert result.month == 5

    result = date_string_to_date("31 5")
    assert result.day == 31
    assert result.month == 5

    result = date_string_to_date("31:05", ":")
    assert result.day == 31
    assert result.month == 5

    with pytest.raises(WeatherError) as error:
        date_string_to_date("45 01")
    assert type(error.value.error_code) == ErrorCode

    with pytest.raises(WeatherError) as error:
        date_string_to_date("13 15")
    assert type(error.value.error_code) == ErrorCode

    with pytest.raises(WeatherError) as error:
        date_string_to_date("14: 15 31")
    assert type(error.value.error_code) == ErrorCode
Example #9
0
def get_date_with_year(day: int, month: int, can_be_past_date: bool = False) -> datetime.date:
    """
    Takes a day and a month without a year and outputs a datetime.date with the year. The year is
    the current year unless the date has already passed, then the boolean switch decides. If that is True
    the year will be the same year, if it is False then it might return the next.

    Wrong dates (like the 31.06.) will not throw an error but instead give a valid day the next month based
    on the number of days it differs from today (so the 31.02. turns into 01.07).

    Args:
        day: number of day - int
        month: number of month - int
        can_be_past_date: True if dates before today should be in the past, else False (default)

    Returns: date as datetime.date
    """
    today = datetime.datetime.now(get_config().timezone).date()
    delta_days = day - today.day
    delta_month = month - today.month
    delta_year = 0
    if not can_be_past_date:
        if delta_month < 0 or delta_days < 0:
            delta_year = 1

    return today + relativedelta(years=delta_year, months=delta_month, days=delta_days)
Example #10
0
def get_weather_forecast(args):
    config = get_config()

    if config is None:
        return "Configuration could not be read. Please make sure everything is set up correctly"

    log.info("Parsing rhasspy intent")

    if args.json is not None:
        if args.json == "-":
            # read and parse json from stdin and send it to rhasspy_weather
            data = json.load(sys.stdin)
        else:
            data = json.loads(args.json)
        request = parse_intent_message(data)
    else:
        #        request = config.parser.parse_cli_args(args)  # if the parser got moved to rhasspy_weather it would be called like this
        request = parse_cli_args(args)

    if request.status.is_error:
        return request.status.status_response()

    log.info("Requesting weather")
    forecast = config.api.get_weather(request.location)
    if forecast.status.is_error:
        return forecast.status.status_response()

    log.info("Formulating answer")
    response = WeatherReport(request, forecast).generate_report()

    return response
Example #11
0
 def __init__(self, error_code: ErrorCode, description: str = ""):
     from rhasspy_weather.data_types.config import get_config
     locale = get_config().locale
     self.description = description
     self.error_code = error_code
     self.message = random.choice(locale.status_response[error_code])
     log.error(description)
Example #12
0
def test_weekday_to_date(mock_config_detail_true):
    config = get_config()
    for weekday in config.locale.weekday_names:
        result = weekday_to_date(weekday)
        assert result.weekday() == config.locale.weekday_names.index(weekday)

    today = datetime.datetime.now(tz=config.timezone).date()
    assert weekday_to_date(config.locale.weekday_names[today.weekday()]) == today + datetime.timedelta(days=7)
Example #13
0
 def __init__(self, severity, description, condition_type: ConditionType):
     self.severity = severity
     self.description = description
     if description == "":
         try:
             self.description = get_config(
             ).locale.conditions[condition_type][severity]
         except KeyError:
             self.description = ""
     self.condition_type = condition_type
Example #14
0
    def format_for_output(self, sentence="{article} {noun} {verb}"):
        from rhasspy_weather.data_types.config import get_config
        locale = get_config().locale

        return utils.remove_excessive_whitespaces(
            sentence.format(article=self.article,
                            noun=self.name,
                            verb=locale.grammar[self.noun_type],
                            when="{when}",
                            where="{where}",
                            weather="{weather}"))
Example #15
0
def test_named_time_to_time(mock_config_detail_true):
    config = get_config()
    for named_time in config.locale.named_times.keys():
        named_time_value = config.locale.named_times[named_time]
        assert named_time_value == named_time_to_time(named_time)

    for named_time in config.locale.named_times_synonyms.keys():
        named_time_value = config.locale.named_times[config.locale.named_times_synonyms[named_time]]
        assert named_time_value == named_time_to_time(named_time)

    with pytest.raises(WeatherError) as error:
        named_time_to_time("blah")
    assert type(error.value.error_code) == ErrorCode
def parse_item_intent(args: json) -> WeatherRequest:
    """
    Parses rhasspy item weather intent

    Args:
        args: the rhasspy intent message

    Returns: WeatherRequest object

    """
    locale = get_config().locale
    new_request = parse_general_intent(args)

    new_request.forecast_type = ForecastType.ITEM
    arg_item = args.item
    if arg_item is not None:
        new_request.requested = parse_item(arg_item, locale)
    return new_request
def parse_item_intent(intent_message: dict) -> WeatherRequest:
    """
    Parses rhasspy item weather intent

    Args:
        intent_message: the rhasspy intent message

    Returns: WeatherRequest object

    """
    locale = get_config().locale
    new_request = parse_general_intent(intent_message)

    slots = intent_message["slots"]
    new_request.forecast_type = ForecastType.ITEM
    if slot_names["item"] in slots:
        new_request.requested = parse_item(slots[slot_names["item"]], locale)
    return new_request
def parse_condition_intent(args: json) -> WeatherRequest:
    """
    Parses rhasspy condition weather intent.

    Args:
        args: the rhasspy intent message

    Returns: WeatherRequest object

    """
    locale = get_config().locale
    new_request = parse_general_intent(args)

    new_request.forecast_type = ForecastType.CONDITION
    arg_condition = args.condition
    if arg_condition is not None:
        new_request.requested = parse_condition(arg_condition, locale)
    return new_request
def parse_temperature_intent(args: json) -> WeatherRequest:
    """
    Parses rhasspy temperature weather intent

    Args:
        args: the rhasspy intent message

    Returns: WeatherRequest object

    """
    locale = get_config().locale
    new_request = parse_general_intent(args)

    new_request.forecast_type = ForecastType.TEMPERATURE
    arg_temperature = args.temperature
    if arg_temperature is not None:
        new_request.requested = parse_temperature(arg_temperature, locale)
    return new_request
Example #20
0
def fill_template(weather_input,
                  result,
                  template_override=None,
                  remove_not_replaced_lines=True):
    config = get_config()
    if template_override is None:
        template = Template(config.output_template)
    else:
        template = Template(template_override)
    template_values = {}
    if type(result) == WeatherError:
        template_values = {
            **template_values,
            **weather_error_to_template_values(result)
        }
    else:
        template_values = {
            **template_values,
            **weather_report_to_template_values(result),
            **weather_object_to_template_values(result.request, "request")
        }
    template_values = {
        **template_values,
        **config.parser.get_template_values(weather_input)
    }
    output = template.safe_substitute(template_values)
    if "\n" in output and remove_not_replaced_lines:
        output_array = output.splitlines()
        new_output = ""
        for item in output_array:
            if "$" in item:
                pass
            else:
                if new_output == "":
                    new_output = item
                else:
                    new_output = new_output + "\n" + item
        output = new_output
    if ".json" in config.output_template_name and template_override is None:
        output = output.replace("None", "null").replace("'", "\"")
        output = json.dumps(
            json.loads(output), ensure_ascii=False
        )  # not the best way but otherwise the json is not correct
    return output
def parse_condition_intent(intent_message: dict) -> WeatherRequest:
    """
    Parses rhasspy condition weather intent.

    Args:
        intent_message: the rhasspy intent message

    Returns: WeatherRequest object

    """
    locale = get_config().locale
    new_request = parse_general_intent(intent_message)

    slots = intent_message["slots"]
    new_request.forecast_type = ForecastType.CONDITION
    if slot_names["condition"] in slots:
        new_request.requested = parse_condition(slots[slot_names["condition"]],
                                                locale)
    return new_request
Example #22
0
    def __init__(self, wind_speed, wind_direction):
        config = get_config()
        if config.units == "imperial":
            wind_speed = wind_speed / 2.237
        severity = normal_round((wind_speed / 0.836) * (2 / 3))

        compass_index = int((wind_direction / 45) + 0.5) % 8
        compass_directions = ["N", "NE", "E", "SE", "S", "SW", "W", "NW"]

        self.wind_speed = wind_speed
        self.wind_direction = compass_directions[compass_index]

        description = ""
        try:
            self.description = config.locale.conditions[
                ConditionType.WIND][severity]
        except KeyError:
            self.description = ""
        super().__init__(severity, description, ConditionType.WIND)
Example #23
0
def named_time_to_str(named_time: str) -> str:
    """
    Takes a named_time and formats it for output. If named time not in locale, it returns the input.
    Args:
        named_time: a valid named time(locale.named_times or locale.named_times_synonyms)

    Returns: the formatted string

    """
    locale = get_config().locale
    named_times_lowercase = [x.lower() for x in locale.named_times.keys()]
    named_times_synonyms_lowercase = [x.lower() for x in locale.named_times_synonyms.keys()]

    if named_time.lower() in named_times_synonyms_lowercase:
        index = named_times_synonyms_lowercase.index(named_time.lower())
        return list(locale.named_times_synonyms.keys())[index]
    elif named_time.lower() in named_times_lowercase:
        index = named_times_lowercase.index(named_time.lower())
        return list(locale.named_times.keys())[index]

    return named_time
Example #24
0
def named_day_to_str(named_day: str) -> str:
    """
    Takes a named day and formats it for output.
    If named day is not in locale, returns the input

    Args:
        named_day: string containing a valid named day (locale.named_days and locale.named_days_synonyms)

    Returns: named_day formatted for output
    """
    locale = get_config().locale
    named_days_lowercase = [x.lower() for x in locale.named_days]
    named_days_synonyms_lowercase = [x.lower() for x in locale.named_days_synonyms]
    if named_day.lower() in named_days_synonyms_lowercase:
        index = named_days_synonyms_lowercase.index(named_day.lower())
        return list(locale.named_days_synonyms.keys())[index]
    elif named_day.lower() in named_days_lowercase:
        index = named_days_lowercase.index(named_day.lower())
        return list(locale.named_days.keys())[index]

    return named_day
Example #25
0
def date_string_to_date(input_string: str, separator: str = " ") -> datetime.date:
    """
    Takes strings in format 'day[separator]month' and parses them as a date. If the input can't be parsed
    into a date a WeatherError occurs.

    Args:
        input_string: string to be parsed
        separator: an (optional) separator between day and month, default is ' '

    Returns: date in the form of datetime.date

    """
    locale = get_config().locale
    try:
        day, month = input_string.split(separator)
    except ValueError:
        raise WeatherError(ErrorCode.DATE_ERROR, "Unknown format for day")

    months_lowercase = [x.lower() for x in locale.month_names]
    if month.lower() in months_lowercase:
        month_number = months_lowercase.index(month.lower()) + 1
    elif month.isnumeric():
        month_number = int(month)
    else:
        log.error("Unknown format for month")
        raise WeatherError(ErrorCode.DATE_ERROR)

    if not 1 <= month_number <= 12:
        log.error("There are exactly 12 months, but the specified date was outside of that")
        raise WeatherError(ErrorCode.DATE_ERROR, "There are exactly 12 months, but the specified date was outside of that")
    if day.isnumeric():
        day_number = int(day)
        if not 1 <= day_number <= 31:
            log.error("Days of the Month can only be between 1 and 31.")
            raise WeatherError(ErrorCode.DATE_ERROR, "Days of the Month can only be between 1 and 31.")
    else:
        log.error("Unknown format for day")
        raise WeatherError(ErrorCode.DATE_ERROR, "Unknown format for day")
    return get_date_with_year(day_number, month_number)
Example #26
0
def get_request(weather_input, config_path: str = None) -> WeatherRequest:
    """
    Function that takes any valid input (see parsers for what can be used here) and returns a WeatherRequest
    Args:
        weather_input: anything that a parser exists for
        config_path: optional path to a config file

    Returns:
        WeatherRequest containing the information from weather_input

    Raises:
        WeatherError: the universal error for this library, more information about what went wrong can be found in the log or inside the error object

    """
    if config_path is not None and cf.config_path is not config_path:
        cf.set_config_path(config_path)

    config = cf.get_config()
    log.info("Parsing input")
    request = config.parser.parse_intent_message(weather_input)

    return request
Example #27
0
def test_get_date_with_year(mock_config_detail_true):
    config = get_config()
    today = datetime.datetime.now(tz=config.timezone).date()

    test_1 = get_date_with_year(today.day - 1, today.month)
    assert test_1.day == today.day - 1
    assert test_1.month == today.month
    assert test_1.year == today.year + 1

    test_2 = get_date_with_year(today.day + 1, today.month)
    assert test_2.day == today.day + 1
    assert test_2.month == today.month
    assert test_2.year == today.year

    test_3 = get_date_with_year(today.day - 1, today.month, True)
    assert test_3.day == today.day - 1
    assert test_3.month == today.month
    assert test_3.year == today.year

    test_4 = get_date_with_year(today.day + 1, today.month, True)
    assert test_4.day == today.day + 1
    assert test_4.month == today.month
    assert test_4.year == today.year
Example #28
0
def get_weather(request: WeatherRequest, config_path: str = None) -> Weather:
    """
    Function taking a WeatherRequest and returning the weather information for the time around the request

    Args:
        request: WeatherRequest object
        config_path: optional path to a config file

    Returns:
        Weather object

    Raises:
        WeatherError: the universal error for this library, more information about what went wrong can be found in the log or inside the error object

    """
    if config_path is not None and cf.config_path is not config_path:
        cf.set_config_path(config_path)

    config = cf.get_config()
    log.info("Requesting weather")
    forecast = config.api.get_weather(request.location)

    return forecast
Example #29
0
def date_string_to_str(input_string: str, separator: str = " ") -> str:
    """
    Takes strings in format 'day[separator]month' and formats them for output depending on how day and month are specified.
    If the month is specified as a month name it will output 'day. month', if month is numeric it will output 'day.month'.
    If neither is the case it will just return the input string.

    This function does not check if the date is valid. If '01 13' is used as an input '01.13' will be returned.

    Args:
        input_string: string to be formatted for output
        separator: an (optional) separator between day and month, default is ' '

    Returns: formatted output or the input string
    """
    locale = get_config().locale
    day, month = input_string.split(separator)
    months_lowercase = [x.lower() for x in locale.month_names]

    if month.lower() in months_lowercase:
        return day + ". " + locale.month_names[months_lowercase.index(month.lower())]
    elif month.isnumeric():
        return day + "." + month

    return input_string
Example #30
0
def test_named_day_to_date(mock_config_detail_true):
    config = get_config()
    today = datetime.datetime.now(tz=config.timezone).date()
    for named_day in config.locale.named_days.keys():
        result = named_day_to_date(named_day)
        named_day_value = config.locale.named_days[named_day]
        if type(named_day_value) == int:
            offset = datetime.timedelta(named_day_value)
            assert today + offset == result
        elif type(named_day_value) == datetime.date:
            assert named_day_value == result

    for named_day in config.locale.named_days_synonyms.keys():
        result = named_day_to_date(named_day)
        named_day_value = config.locale.named_days[config.locale.named_days_synonyms[named_day]]
        if type(named_day_value) == int:
            offset = datetime.timedelta(named_day_value)
            assert today + offset == result
        elif type(named_day_value) == datetime.date:
            assert named_day_value == result

    with pytest.raises(WeatherError) as error:
        named_day_to_date("blah")
    assert type(error.value.error_code) == ErrorCode