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
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)
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
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
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")
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)
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
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)
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
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)
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)
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
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}"))
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
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
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)
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
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
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)
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
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
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
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
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