├── .gitignore ├── AUTHORS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── action-owm.py ├── config.ini.default ├── requirements.txt ├── setup.sh ├── snipsowm ├── __init__.py ├── feedback │ ├── __init__.py │ ├── sentence_generation_utils.py │ └── sentence_generator.py ├── owm.py ├── provider │ ├── __init__.py │ ├── owm_provider.py │ ├── providers.py │ └── weather_provider.py ├── sentence_generator.py ├── snips.py ├── snipsowm.py ├── utils.py ├── wagnerfischerpp.py ├── weather.py └── weather_condition.py └── tests ├── __init__.py ├── feedback_test ├── __init__.py └── sentence_generator_test.py ├── provider_test ├── __init__.py └── owm_provider_test.py └── test_skill.py /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | .idea/ 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | 49 | # Translations 50 | *.mo 51 | *.pot 52 | 53 | # Django stuff: 54 | *.log 55 | local_settings.py 56 | 57 | # Flask stuff: 58 | instance/ 59 | .webassets-cache 60 | 61 | # Scrapy stuff: 62 | .scrapy 63 | 64 | # Sphinx documentation 65 | docs/_build/ 66 | 67 | # PyBuilder 68 | target/ 69 | 70 | # Jupyter Notebook 71 | .ipynb_checkpoints 72 | 73 | # pyenv 74 | .python-version 75 | 76 | # celery beat schedule file 77 | celerybeat-schedule 78 | 79 | # SageMath parsed files 80 | *.sage.py 81 | 82 | # dotenv 83 | .env 84 | 85 | # virtualenv 86 | .venv 87 | venv/ 88 | ENV/ 89 | 90 | # Spyder project settings 91 | .spyderproject 92 | .spyproject 93 | 94 | # Rope project settings 95 | .ropeproject 96 | 97 | # mkdocs documentation 98 | /site 99 | 100 | # mypy 101 | .mypy_cache/ 102 | 103 | src/ 104 | temp/ 105 | .DS_Store 106 | .AppleDouble/ 107 | Makefile 108 | 109 | config.ini -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | snips-skill-owm is written and maintained by Snips. 2 | 3 | Development Lead 4 | ~~~~~~~~~~~~~~~~ 5 | 6 | - Anthony Reinette 7 | - Robin Guignard-Perret 8 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at support@snips.ai. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | How to Contribute 2 | ================= 3 | 4 | Contributions are welcome! Not familiar with the codebase yet? No problem! 5 | There are many ways to contribute to open source projects: reporting bugs, 6 | helping with the documentation, spreading the word and of course, adding 7 | new features and patches. 8 | 9 | Getting Started 10 | --------------- 11 | * Make sure you have a GitHub account. 12 | * Open a [new issue](https://github.com/snipsco/snips-skill-owm/issues), assuming one does not already exist. 13 | * Clearly describe the issue including steps to reproduce when it is a bug. 14 | 15 | Making Changes 16 | -------------- 17 | * Fork this repository. 18 | * Create a feature branch from where you want to base your work. 19 | * Make commits of logical units (if needed rebase your feature branch before 20 | submitting it). 21 | * Check for unnecessary whitespace with ``git diff --check`` before committing. 22 | * Make sure your commit messages are well formatted. 23 | * If your commit fixes an open issue, reference it in the commit message (f.e. `#15`). 24 | * Run all the tests (if existing) to assure nothing else was accidentally broken. 25 | 26 | These guidelines also apply when helping with documentation. 27 | 28 | Submitting Changes 29 | ------------------ 30 | * Push your changes to a feature branch in your fork of the repository. 31 | * Submit a `Pull Request`. 32 | * Wait for maintainer feedback. 33 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Snips 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.txt 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # OpenWeatherMap action for Snips 2 | 3 | [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](https://raw.githubusercontent.com/snipsco/snips-skill-owm/master/LICENSE.txt) 4 | 5 | ## Installation with Sam 6 | 7 | The easiest way to use this Action is to install it with [Sam](https://snips.gitbook.io/getting-started/installation) 8 | 9 | `sam install actions -g https://github.com/snipsco/snips-skill-owm.git` 10 | 11 | Sam will then ask you for an OpenWeatherMap API key. You can create one by signing up to [OpenWeatherMap](https://openweathermap.org) 12 | 13 | The action works with the English Weather skill that you can download on [Snips' console](https://console.snips.ai) 14 | 15 | ## Locale 16 | > ***BEWARE: Please do not forget that you have to specify one of the following values as the locale setting during the installation. If you install it manually, please do give this setting to `config.ini` -> `locale=`. Otherwise, it will not work as expected.*** 17 | 18 | To have the skills properly working, you **need** to generate locales for your languages. So far the supported locales are: 19 | 20 | - 🇺🇸 `en_US` 21 | - 🇫🇷 `fr_FR` 22 | - 🇪🇸 `es_ES` 23 | 24 | You can generate them with `sudo raspi-config`. Going in the `Localisation Options` submenu, then in the `Change Locale` submenu, and selecting the locales you want to support. For instance, select `en_US UTF-8` if you want support for English. 25 | 26 | ## Manual installation 27 | 28 | - Clone the repository on your Pi 29 | - Run `setup.sh` (it will create a virtualenv, install the dependencies in it and rename config.ini.default to config.ini) 30 | - Provide an OpenWeatherMap API key in the config.ini 31 | - Run `action-owm.py` 32 | 33 | ## Contributing 34 | 35 | Please see the [Contribution Guidelines](https://github.com/snipsco/snips-skill-owm/blob/master/CONTRIBUTING.md). 36 | 37 | ## Copyright 38 | 39 | This action is provided by [Snips](https://www.snips.ai) as Open Source software. See [LICENSE.txt](https://github.com/snipsco/snips-skill-owm/blob/master/LICENSE.txt) for more information. 40 | -------------------------------------------------------------------------------- /action-owm.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python2 2 | # -*-: coding utf-8 -*- 3 | 4 | import ConfigParser 5 | from datetime import datetime 6 | import datetime as dt 7 | from dateutil.parser import parse 8 | from hermes_python.hermes import Hermes 9 | import hermes_python 10 | import io 11 | import os 12 | import sys 13 | from snipsowm.snipsowm import SnipsOWM 14 | 15 | CONFIGURATION_ENCODING_FORMAT = "utf-8" 16 | CONFIG_INI = "config.ini" 17 | 18 | MQTT_IP_ADDR = "localhost" 19 | MQTT_PORT = 1883 20 | MQTT_ADDR = "{}:{}".format(MQTT_IP_ADDR, str(MQTT_PORT)) 21 | 22 | DIR = os.path.dirname(os.path.realpath(__file__)) + '/alarm/' 23 | 24 | lang = "EN" 25 | 26 | 27 | class SnipsConfigParser(ConfigParser.SafeConfigParser): 28 | def to_dict(self): 29 | return {section: {option_name : option for option_name, option in self.items(section)} for section in self.sections()} 30 | 31 | def read_configuration_file(configuration_file): 32 | try: 33 | with io.open(configuration_file, encoding=CONFIGURATION_ENCODING_FORMAT) as f: 34 | conf_parser = SnipsConfigParser() 35 | conf_parser.readfp(f) 36 | return conf_parser.to_dict() 37 | except (IOError, ConfigParser.Error) as e: 38 | return dict() 39 | 40 | def getCondition(snips): 41 | # Determine condition 42 | if snips.slots.forecast_condition_name: 43 | res = snips.slots.forecast_condition_name[0].slot_value.value.value 44 | return unicode(res) 45 | return None 46 | 47 | def getLocality(snips): 48 | if snips.slots.forecast_locality: 49 | res = snips.slots.forecast_locality[0].slot_value.value.value 50 | return unicode(res) 51 | return None 52 | 53 | def getRegion(snips): 54 | if snips.slots.forecast_region: 55 | res = snips.slots.forecast_region[0].slot_value.value.value 56 | return unicode(res) 57 | return None 58 | 59 | def getCountry(snips): 60 | if snips.slots.forecast_country : 61 | res = snips.slots.forecast_country[0].slot_value.value.value 62 | return unicode(res) 63 | return None 64 | 65 | def getPOI(snips): 66 | if snips.slots.forecast_geographical_poi: 67 | res = snips.slots.forecast_geographical_poi[0].slot_value.value.value 68 | return unicode(res) 69 | return None 70 | 71 | def getItemName(snips): 72 | if snips.slots.forecast_item: 73 | res = snips.slots.forecast_item[0].slot_value.value.value 74 | return unicode(res) 75 | return None 76 | 77 | def getDateTime(snips): 78 | # Determine datetime 79 | if snips.slots.forecast_start_datetime: 80 | tmp = snips.slots.forecast_start_datetime[0].slot_value.value 81 | if tmp is None: 82 | return None 83 | if isinstance(tmp, hermes_python.ontology.dialogue.InstantTimeValue ): 84 | val = tmp.value[:-7] 85 | return datetime.strptime(val, '%Y-%m-%d %H:%M:%S') 86 | elif isinstance(tmp, hermes_python.ontology.dialogue.TimeIntervalValue ): 87 | t0 = tmp.from_date[:-7] 88 | t0 = datetime.strptime(t0, '%Y-%m-%d %H:%M:%S') 89 | t1 = tmp.to_date[:-7] 90 | t1 = datetime.strptime(t1, '%Y-%m-%d %H:%M:%S') 91 | delta = t1 - t0 92 | return t0 + delta / 2 93 | return None 94 | 95 | def getAnyLocality(snips): 96 | locality = None 97 | try: 98 | locality = snips.slots.forecast_locality \ 99 | or snips.slots.forecast_country \ 100 | or snips.slots.forecast_region \ 101 | or snips.slots.forecast_geographical_poi 102 | 103 | if locality: 104 | return unicode(locality[0].slot_value.value.value) 105 | except Exception: 106 | pass 107 | 108 | def getGranurality(datetime): 109 | # Determine granularity 110 | if datetime: # We have an information about the date. 111 | now = dt.datetime.now().replace(tzinfo=None) 112 | delta_days = abs((datetime - now).days) 113 | if delta_days > 10: # There a week difference between today and the date we want the forecast. 114 | return 2 # Give the day of the forecast date, plus the number of the day in the month. 115 | elif delta_days > 5: # There a 10-day difference between today and the date we want the forecast. 116 | return 1 # Give the full date 117 | else: 118 | return 0 # Just give the day of the week 119 | else: 120 | return 0 121 | 122 | def searchWeatherForecastTemperature(hermes, intent_message): 123 | datetime = getDateTime(intent_message) 124 | granularity = getGranurality(datetime) 125 | locality = getAnyLocality(intent_message) 126 | res = hermes.skill.speak_temperature(locality, datetime, granularity) 127 | current_session_id = intent_message.session_id 128 | hermes.publish_end_session(current_session_id, res.decode("latin-1")) 129 | 130 | def searchWeatherForecastCondition(hermes, intent_message): 131 | datetime = getDateTime(intent_message) 132 | granularity = getGranurality(datetime) 133 | condition = getCondition(intent_message) 134 | locality = getLocality(intent_message) 135 | region = getRegion(intent_message) 136 | country = getCountry(intent_message) 137 | geographical_poi = getPOI(intent_message) 138 | res = hermes.skill.speak_condition(condition, datetime, 139 | granularity=granularity, Locality=locality, 140 | Region=region, Country=country, 141 | POI=geographical_poi) 142 | current_session_id = intent_message.session_id 143 | hermes.publish_end_session(current_session_id, res.decode("latin-1")) 144 | 145 | def searchWeatherForecast(hermes, intent_message): 146 | datetime = getDateTime(intent_message) 147 | granularity = getGranurality(datetime) 148 | # No condition in this intent so initialized to None 149 | condition_name = None 150 | locality = getLocality(intent_message) 151 | region = getRegion(intent_message) 152 | country = getCountry(intent_message) 153 | geographical_poi = getPOI(intent_message) 154 | res = hermes.skill.speak_condition(condition_name, datetime, 155 | granularity=granularity, Locality=locality, 156 | Region=region, Country=country, 157 | POI=geographical_poi) 158 | current_session_id = intent_message.session_id 159 | hermes.publish_end_session(current_session_id, res.decode("latin-1")) 160 | 161 | def searchWeatherForecastItem(hermes, intent_message): 162 | datetime = getDateTime(intent_message) 163 | granularity = getGranurality(datetime) 164 | item_name = getItemName(intent_message) 165 | locality = getLocality(intent_message) 166 | region = getRegion(intent_message) 167 | country = getCountry(intent_message) 168 | geographical_poi = getPOI(intent_message) 169 | res = hermes.skill.speak_item(item_name, 170 | datetime, 171 | granularity=granularity, 172 | Locality=locality, 173 | Region=region, 174 | Country=country, 175 | POI=geographical_poi) 176 | current_session_id = intent_message.session_id 177 | hermes.publish_end_session(current_session_id, res.decode("latin-1")) 178 | 179 | if __name__ == "__main__": 180 | config = read_configuration_file("config.ini") 181 | 182 | if config.get("secret").get("api_key") is None: 183 | print "No API key in config.ini, you must setup an OpenWeatherMap API key for this skill to work" 184 | elif len(config.get("secret").get("api_key")) == 0: 185 | print "No API key in config.ini, you must setup an OpenWeatherMap API key for this skill to work" 186 | 187 | skill_locale = config.get("secret", {"locale":"en_US"}).get("locale", u"en_US") 188 | 189 | if skill_locale == u"": 190 | print "No locale information is found!" 191 | print "Please edit 'config.ini' file, give either en_US, fr_FR or es_ES refering to the language of your assistant" 192 | sys.exit(1) 193 | 194 | skill = SnipsOWM(config["secret"]["api_key"], 195 | config["secret"]["default_location"],locale=skill_locale.decode('ascii')) 196 | 197 | lang = "EN" 198 | with Hermes(MQTT_ADDR.encode("ascii")) as h: 199 | h.skill = skill 200 | h.subscribe_intent("searchWeatherForecastItem", 201 | searchWeatherForecastItem) \ 202 | .subscribe_intent("searchWeatherForecastTemperature", 203 | searchWeatherForecastTemperature) \ 204 | .subscribe_intent("searchWeatherForecastCondition", 205 | searchWeatherForecastCondition) \ 206 | .subscribe_intent("searchWeatherForecast", searchWeatherForecast) \ 207 | .loop_forever() 208 | -------------------------------------------------------------------------------- /config.ini.default: -------------------------------------------------------------------------------- 1 | [global] 2 | 3 | [secret] 4 | api_key= 5 | locale= 6 | default_location= 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2017.7.27.1 2 | chardet==3.0.4 3 | enum34==1.1.6 4 | idna==2.6 5 | requests==2.18.4 6 | urllib3==1.22 7 | hermes_python 8 | python-dateutil==2.7.3 -------------------------------------------------------------------------------- /setup.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash -e 2 | 3 | VENV=venv 4 | 5 | if [ ! -d "$VENV" ] 6 | then 7 | 8 | PYTHON=`which python2` 9 | 10 | if [ ! -f $PYTHON ] 11 | then 12 | echo "could not find python" 13 | fi 14 | virtualenv -p $PYTHON $VENV 15 | 16 | fi 17 | 18 | . $VENV/bin/activate 19 | 20 | 21 | pip install -r requirements.txt 22 | 23 | if [ ! -e config.ini ] 24 | then 25 | cp config.ini.default config.ini 26 | fi 27 | -------------------------------------------------------------------------------- /snipsowm/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/snipsowm/__init__.py -------------------------------------------------------------------------------- /snipsowm/feedback/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/snipsowm/feedback/__init__.py -------------------------------------------------------------------------------- /snipsowm/feedback/sentence_generation_utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | def date_to_string(date, granularity=0): 3 | """ Convert a date to a string, with an appropriate level of 4 | granularity. 5 | 6 | :param date: A datetime object. 7 | :param granularity: Granularity for the desired textual representation. 8 | 0: precise (date and time are returned) 9 | 1: day (only the week day is returned) 10 | 2: month (only year and month are returned) 11 | 3: year (only year is returned) 12 | :return: A textual representation of the date. 13 | """ 14 | if not date: 15 | return "" 16 | 17 | if granularity == 0: 18 | return date.strftime("%A") 19 | elif granularity == 1: 20 | return date.strftime("%A, %d") 21 | elif granularity == 2: 22 | return date.strftime("%A, %d %B") 23 | 24 | return date.strftime("%A, %d %B, %H:%M%p") 25 | 26 | 27 | def french_is_masculine_word(word): 28 | return word[len(word) - 1] not in ['é', 'e'] 29 | 30 | 31 | def starts_with_vowel(word): 32 | return word[0] in ['a', 'e', 'i', 'o', 'u', 'y'] -------------------------------------------------------------------------------- /snipsowm/feedback/sentence_generator.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import abc 3 | import collections 4 | from enum import Enum 5 | import locale 6 | import sentence_generation_utils as utils 7 | import random 8 | 9 | 10 | class SentenceGenerator(object): 11 | __metaclass__ = abc.ABCMeta 12 | 13 | def __init__(self, locale="en_US"): 14 | self.locale = locale 15 | 16 | def generate_error_sentence(self): 17 | error_sentences = { 18 | "en_US": "An error occured when trying to retrieve the weather, please try again", 19 | "fr_FR": "Désolé, il y a eu une erreur lors de la récupération des données météo. Veuillez réessayer", 20 | "es_ES": "Ha ocurrido un error obteniendo la información meteorológica, por favor inténtalo de nuevo" 21 | } 22 | 23 | return error_sentences[self.locale] 24 | 25 | def generate_error_locale_sentence(self): 26 | """ 27 | TODO : This method is too specific to be implemented by the SentenceGenerator class. 28 | :return: 29 | :rtype:basestring 30 | """ 31 | error_sentences = { 32 | "en_US": "An error occured. Your system doesn't have the correct locale installed. Please refer to the documentation. ", 33 | "fr_FR": "Désolé, il y a eu une erreur. Votre système n'a pas la locale correcte installée. Veuillez consulter la documentation pour plus de détails.", 34 | "es_ES": "Ha ocurrido un error. Parece que el sistema no tiene instalado el paquete de idioma correcto ('locale'). Consula la documentación para más detalles." 35 | } 36 | 37 | return error_sentences[self.locale] 38 | 39 | def generate_api_key_error_sentence(self): 40 | error_sentences = { 41 | "en_US": "The API key you provided is invalid, check your config.ini", 42 | "fr_FR": "La clé API fournie est incorrecte, vérifiez le fichier config.ini", 43 | "es_ES": "La clave de la API proporcionada no es válida, por favor comprueba tu fichero config.ini" 44 | } 45 | return error_sentences[self.locale] 46 | 47 | 48 | class SimpleSentenceGenerator(SentenceGenerator): 49 | pass 50 | 51 | 52 | class AnswerSentenceGenerator(SentenceGenerator): 53 | class SentenceTone(Enum): 54 | NEUTRAL = 0 55 | POSITIVE = 1 56 | NEGATIVE = 2 57 | 58 | def generate_sentence_introduction(self, tone): 59 | """ 60 | 61 | :param tone: 62 | :type tone: SentenceTone 63 | :return: an introduction string 64 | :rtype:basestring 65 | """ 66 | 67 | sentence_beginnings = { 68 | "en_US": { 69 | AnswerSentenceGenerator.SentenceTone.POSITIVE: "Yes,", 70 | AnswerSentenceGenerator.SentenceTone.NEGATIVE: "No,", 71 | AnswerSentenceGenerator.SentenceTone.NEUTRAL: "" 72 | }, 73 | "fr_FR": { 74 | AnswerSentenceGenerator.SentenceTone.POSITIVE: "Oui,", 75 | AnswerSentenceGenerator.SentenceTone.NEGATIVE: "Non,", 76 | AnswerSentenceGenerator.SentenceTone.NEUTRAL: "" 77 | }, 78 | "es_ES": { 79 | AnswerSentenceGenerator.SentenceTone.POSITIVE: "Sí,", 80 | AnswerSentenceGenerator.SentenceTone.NEGATIVE: "No,", 81 | AnswerSentenceGenerator.SentenceTone.NEUTRAL: "" 82 | } 83 | } 84 | 85 | sentence_beginning = sentence_beginnings[self.locale][tone] 86 | return sentence_beginning 87 | 88 | def generate_sentence_locality(self, POI=None, Locality=None, Region=None, Country=None): 89 | """ 90 | :param Locality: 91 | :type Locality:basestring 92 | :param POI: 93 | :type POI:basestring 94 | :param Region: 95 | :type Region:basestring 96 | :param Country: 97 | :type Country:basestring 98 | :return: 99 | :rtype: basestring 100 | """ 101 | if self.locale == "en_US": 102 | if (POI or Locality or Region or Country): 103 | locality = filter(lambda x: x is not None, [POI, Locality, Region, Country])[0] 104 | return "in {}".format(locality) 105 | else: 106 | return "" 107 | 108 | elif self.locale == "fr_FR": 109 | """ 110 | Country granularity: 111 | - We use "au" for masculine nouns that begins with a consonant 112 | - We use "en" with feminine words and masculine words that start with a vowel 113 | - Careful with the country Malte. Which is an exception 114 | 115 | Region granularity: 116 | We use the "en" for regions 117 | 118 | Locality granularity: 119 | This is used for cities, We use the "à" preposition. 120 | 121 | POIs granularity: 122 | We use the "à" preposition 123 | 124 | """ 125 | 126 | if POI: 127 | return "à {}".format(POI) 128 | 129 | if Locality: 130 | return "à {}".format(Locality) 131 | 132 | if Region: 133 | if utils.french_is_masculine_word(Region) and (not utils.starts_with_vowel(Region)): 134 | return "au {}".format(Region) 135 | else: 136 | return "en {}".format(Region) 137 | if Country: 138 | if utils.french_is_masculine_word(Country) and (not utils.starts_with_vowel(Country)): 139 | return "au {}".format(Country) 140 | else: 141 | return "en {}".format(Country) 142 | 143 | return "" 144 | 145 | if self.locale == "es_ES": 146 | if POI or Locality or Region or Country: 147 | locality = filter(lambda x: x is not None, [POI, Locality, Region, Country])[0] 148 | return "en {}".format(locality) 149 | else: 150 | return "" 151 | 152 | else: 153 | return "" 154 | 155 | def generate_sentence_date(self, date, granularity=0): 156 | """ Convert a date to a string, with an appropriate level of 157 | granularity. 158 | 159 | :param date: A datetime object. 160 | :param granularity: Granularity for the desired textual representation. 161 | 0: precise (date and time are returned) 162 | 1: day (only the week day is returned) 163 | 2: month (only year and month are returned) 164 | 3: year (only year is returned) 165 | :return: A textual representation of the date. 166 | """ 167 | 168 | full_locale = "{}.UTF-8".format(self.locale) 169 | 170 | try: # Careful, this operation is not thread safe ... 171 | locale.setlocale(locale.LC_TIME, full_locale) 172 | except locale.Error: 173 | print "Careful! There was an error while trying to set the locale {}. This means your locale is not properly installed. Please refer to the README for more information.".format(full_locale) 174 | print "Some information displayed might not be formated to your locale" 175 | 176 | return utils.date_to_string(date, granularity) 177 | 178 | 179 | class ConditionQuerySentenceGenerator(AnswerSentenceGenerator): 180 | 181 | def generate_condition_description(self, condition_description): 182 | return condition_description if len(condition_description) > 0 else "" 183 | 184 | def generate_condition_sentence(self, 185 | tone=AnswerSentenceGenerator.SentenceTone.POSITIVE, 186 | date=None, granularity=0, 187 | condition_description=None, 188 | POI=None, Locality=None, Region=None, Country=None): 189 | """ 190 | The sentence is generated from those parts : 191 | - introduction (We answer positively or negatively to the user) 192 | - condition (we describe the condition to the user) 193 | - date (when is the condition happening) 194 | - locality (where the condition is happening) 195 | 196 | :param tone: 197 | :type tone: SentenceTone 198 | :param condition_description: 199 | :type condition_description: basestring 200 | :param locality: 201 | :type locality:basestring 202 | :param date: 203 | :type date:datetime 204 | :return: 205 | :rtype: 206 | 207 | """ 208 | introduction = self.generate_sentence_introduction(tone) 209 | 210 | locality = self.generate_sentence_locality(POI, Locality, Region, Country) 211 | 212 | date = self.generate_sentence_date(date, granularity=granularity) 213 | 214 | permutable_parameters = list((locality, date)) 215 | random.shuffle(permutable_parameters) 216 | parameters = (introduction, condition_description) + tuple(permutable_parameters) 217 | 218 | # Formatting 219 | parameters = filter(lambda x: not x is None and len(x) > 0, parameters) 220 | return ("{} " * len(parameters)).format(*parameters) 221 | 222 | 223 | class TemperatureQuerySentenceGenerator(AnswerSentenceGenerator): 224 | def generate_temperature_sentence(self, 225 | temperature="-273.15", 226 | date=None, granularity=0, 227 | POI=None, Locality=None, Region=None, Country=None): 228 | """ 229 | The sentence is generated from those parts : 230 | - the temperature for the date and time 231 | - date (the time when their will be such a temperature ) 232 | - locality (the place their is such a temperature) 233 | 234 | :param temperature 235 | :param locality: 236 | :type locality:basestring 237 | :param date: 238 | :type date:datetime 239 | :return: 240 | :rtype: 241 | 242 | """ 243 | error_sentences = { 244 | "en_US": "I couldn't fetch the right data for the specified place and date", 245 | "fr_FR": "Je n'ai pas pu récupérer les prévisions de température pour cet endroit et ces dates", 246 | "es_ES": "No he podido encontrar información meteorológica para el lugar y la fecha especificados" 247 | } 248 | 249 | if (temperature is None): 250 | return error_sentences[self.locale] 251 | 252 | sentence_introductions = { 253 | "en_US": ["The temperature will be {} degrees"], 254 | "fr_FR": ["La température sera de {} degrés", "Il fera {} degrés"], 255 | "es_ES": ["La temperatura será de {} grados", "Habrá {} grados"] 256 | } 257 | 258 | introduction = random.choice(sentence_introductions[self.locale]).format(temperature) 259 | locality = self.generate_sentence_locality(POI, Locality, Region, Country) 260 | date = self.generate_sentence_date(date) 261 | 262 | permutable_parameters = list((locality, date)) 263 | random.shuffle(permutable_parameters) 264 | parameters = (introduction,) + tuple(permutable_parameters) 265 | return "{} {} {}".format(*parameters) 266 | 267 | 268 | class SentenceGenerationException(Exception): 269 | pass 270 | 271 | 272 | class SentenceGenerationLocaleException(SentenceGenerationException): 273 | pass 274 | -------------------------------------------------------------------------------- /snipsowm/owm.py: -------------------------------------------------------------------------------- 1 | from enum import Enum 2 | 3 | class OWMWeatherConditions(Enum): 4 | THUNDERSTORM_WITH_LIGHT_RAIN = 200 5 | THUNDERSTORM_WITH_RAIN = 201 6 | THUNDERSTORM_WITH_HEAVY_RAIN = 202 7 | LIGHT_THUNDERSTORM = 210 8 | THUNDERSTORM = 211 9 | HEAVY_THUNDERSTORM = 212 10 | RAGGED_THUNDERSTORM = 221 11 | THUNDERSTORM_WITH_LIGHT_DRIZZLE = 230 12 | THUNDERSTORM_WITH_DRIZZLE = 231 13 | THUNDERSTORM_WITH_HEAVY_DRIZZLE = 232 14 | LIGHT_INTENSITY_DRIZZLE = 300 15 | DRIZZLE = 301 16 | HEAVY_INTENSITY_DRIZZLE = 302 17 | LIGHT_INTENSITY_DRIZZLE_RAIN = 310 18 | DRIZZLE_RAIN = 311 19 | HEAVY_INTENSITY_DRIZZLE_RAIN = 312 20 | SHOWER_RAIN_AND_DRIZZLE = 313 21 | HEAVY_SHOWER_RAIN_AND_DRIZZLE = 314 22 | SHOWER_DRIZZLE = 321 23 | LIGHT_RAIN = 500 24 | MODERATE_RAIN = 501 25 | HEAVY_INTENSITY_RAIN = 502 26 | VERY_HEAVY_RAIN = 503 27 | EXTREME_RAIN = 504 28 | FREEZING_RAIN = 511 29 | LIGHT_INTENSITY_SHOWER_RAIN = 520 30 | SHOWER_RAIN = 521 31 | HEAVY_INTENSITY_SHOWER_RAIN = 522 32 | RAGGED_SHOWER_RAIN = 531 33 | LIGHT_SNOW = 600 34 | SNOW = 601 35 | HEAVY_SNOW = 602 36 | SLEET = 611 37 | SHOWER_SLEET = 612 38 | LIGHT_RAIN_AND_SNOW = 615 39 | RAIN_AND_SNOW = 616 40 | LIGHT_SHOWER_SNOW = 620 41 | SHOWER_SNOW = 621 42 | HEAVY_SHOWER_SNOW = 622 43 | MIST = 701 44 | SMOKE = 711 45 | HAZE = 721 46 | SAND_DUST_WHIRLS = 731 47 | FOG = 741 48 | SAND = 751 49 | DUST = 761 50 | VOLCANIC_ASH = 762 51 | SQUALLS = 771 52 | TORNAD = 781 53 | CLEAR_SKY = 800 54 | FEW_CLOUDS = 801 55 | SCATTERED_CLOUDS = 802 56 | BROKEN_CLOUDS = 803 57 | OVERCAST_CLOUDS = 804 58 | TORNADO = 900 59 | TROPICAL_STORM = 901 60 | HURRICANE = 902 61 | COLD = 903 62 | HOT = 904 63 | WINDY = 905 64 | HAIL = 906 65 | -------------------------------------------------------------------------------- /snipsowm/provider/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/snipsowm/provider/__init__.py -------------------------------------------------------------------------------- /snipsowm/provider/owm_provider.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | import datetime 3 | import json 4 | import requests 5 | from weather_provider import WeatherProvider, WeatherProviderError, WeatherProviderConnectivityError, WeatherProviderInvalidAPIKey 6 | 7 | 8 | class OWMWeatherProvider(WeatherProvider): 9 | API_WEATHER_ENDPOINT = "http://api.openweathermap.org/data/2.5/weather" 10 | API_FORECAST_ENDPOINT = "http://api.openweathermap.org/data/2.5/forecast" 11 | OWM_MAX_FORECAST_DAYS = 15 12 | 13 | def __init__(self, api_key): 14 | self.api_key = api_key 15 | 16 | def get_current_weather(self, location): 17 | """Perform the API request. 18 | 19 | :param location: The location of the forecast, e.g. 'Paris,fr' or 20 | 'Eiffel Tower' 21 | """ 22 | url = "{}?APPID={}&q={}&units=metric".format(self.API_WEATHER_ENDPOINT, 23 | self.api_key, 24 | location) 25 | r = requests.get(url) 26 | response = json.loads(r.text) 27 | 28 | if response['cod'] == 404: 29 | raise OpenWeatherMapQueryError(response['message']) 30 | 31 | if response['cod'] == 401: 32 | raise OpenWeatherMapAPIKeyError(response['message']) 33 | 34 | try: 35 | description = response["weather"][0]["id"] 36 | except (KeyError, IndexError, UnicodeEncodeError): 37 | description = None 38 | try: 39 | temperature = int(float(response["main"]["temp"])) 40 | except KeyError: 41 | temperature = None 42 | return description, temperature 43 | 44 | def get_forecast_weather(self, location, datetime): 45 | """ 46 | Perform the API request. 47 | :param location: The location of the asked forecast 48 | :type location: string 49 | :param datetime: The date and time of the requested forecast 50 | :type datetime: datetime 51 | :return: description of the asked weather and the temperature 52 | :rtype: (str, int) 53 | """ 54 | url = "{}?APPID={}&q={}&units=metric".format(self.API_FORECAST_ENDPOINT, 55 | self.api_key, 56 | location) 57 | r = requests.get(url) 58 | response = json.loads(r.text) 59 | 60 | if response['cod'] == 404: 61 | raise OpenWeatherMapQueryError(response['message']) 62 | 63 | if response['cod'] == 401: 64 | raise OpenWeatherMapAPIKeyError(response['message']) 65 | 66 | response = self._getTopicalInfos(response, datetime) 67 | 68 | try: 69 | description = response["weather"][0]["id"] 70 | except (KeyError, IndexError, UnicodeEncodeError): 71 | description = None 72 | try: 73 | temperature = int(float(response["main"]["temp"])) 74 | except KeyError: 75 | temperature = None 76 | return description, temperature 77 | 78 | def get_weather(self, location, datetime=None): 79 | if datetime: 80 | self.check_owm_forecast_limit(datetime) 81 | return self.get_forecast_weather(location, datetime) 82 | else: 83 | return self.get_current_weather(location) 84 | 85 | def check_owm_forecast_limit(self, dt): 86 | """ 87 | 88 | :param datetime: 89 | :return:bool 90 | """ 91 | now_date = datetime.datetime.now() 92 | if dt < now_date or (dt - now_date).days >= self.OWM_MAX_FORECAST_DAYS: 93 | raise OpenWeatherMapMaxDaysForecastError( 94 | "OpenWeather map day forecast limit exceed. Can't get forecast for more than {} days".format( 95 | OWMWeatherProvider.OWM_MAX_FORECAST_DAYS)) 96 | 97 | def _getTopicalInfos(self, response, date_time): 98 | delta = date_time - datetime.datetime.strptime(response["list"][0]['dt_txt'], "%Y-%m-%d %H:%M:%S") 99 | delta = abs(delta) 100 | result = {} 101 | 102 | for time_interval in response["list"]: 103 | current_time = datetime.datetime.strptime(time_interval['dt_txt'], "%Y-%m-%d %H:%M:%S") 104 | current_delta = date_time - current_time 105 | current_delta = abs(current_delta) 106 | if delta > current_delta: 107 | delta = current_delta 108 | result = time_interval 109 | 110 | return result 111 | 112 | 113 | class OpenWeatherMapError(WeatherProviderError): 114 | """Basic exception for errors raised by OWM""" 115 | pass 116 | 117 | 118 | class OpenWeatherMapAPIKeyError(WeatherProviderInvalidAPIKey): 119 | """Exception raised when the wrong API key is provided""" 120 | pass 121 | 122 | 123 | class OpenWeatherMapQueryError(OpenWeatherMapError): 124 | """Exception for 404 errors raised by OWM""" 125 | pass 126 | 127 | 128 | class OpenWeatherMapMaxDaysForecastError(OpenWeatherMapError): 129 | """Exception raised by OWM when a forecast for more than OWMWeatherProvider.OWM_MAX_FORECAST_DAYS days is asked""" 130 | pass 131 | -------------------------------------------------------------------------------- /snipsowm/provider/providers.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from owm_provider import OWMWeatherProvider 3 | 4 | from enum import Enum 5 | 6 | class WeatherProviders(Enum): 7 | OWM = OWMWeatherProvider -------------------------------------------------------------------------------- /snipsowm/provider/weather_provider.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import abc 4 | 5 | 6 | class WeatherProvider(object): 7 | __metaclass__ = abc.ABCMeta 8 | 9 | @abc.abstractmethod 10 | def get_current_weather(self): 11 | pass 12 | @abc.abstractmethod 13 | def get_forecast_weather(self): 14 | pass 15 | 16 | @abc.abstractmethod 17 | def get_weather(self): 18 | pass 19 | 20 | 21 | class WeatherProviderError(Exception): 22 | pass 23 | 24 | 25 | class WeatherProviderConnectivityError(Exception): 26 | pass 27 | 28 | 29 | class WeatherProviderInvalidAPIKey(Exception): 30 | pass 31 | -------------------------------------------------------------------------------- /snipsowm/sentence_generator.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # -*- coding: utf-8 -*- 3 | import datetime 4 | from enum import Enum 5 | import locale 6 | import random 7 | 8 | 9 | class SentenceTone(Enum): 10 | NEUTRAL = 0 11 | POSITIVE = 1 12 | NEGATIVE = 2 13 | 14 | 15 | def date_to_string(date, granularity=0): 16 | """ Convert a date to a string, with an appropriate level of 17 | granularity. 18 | 19 | :param date: A datetime object. 20 | :param granularity: Granularity for the desired textual representation. 21 | 0: precise (date and time are returned) 22 | 1: day (only the week day is returned) 23 | 2: month (only year and month are returned) 24 | 3: year (only year is returned) 25 | :return: A textual representation of the date. 26 | """ 27 | if not date: 28 | return "" 29 | 30 | if granularity == 0: 31 | return date.strftime("%A") 32 | elif granularity == 1: 33 | return date.strftime("%A, %d") 34 | elif granularity == 2: 35 | return date.strftime("%A, %d %B") 36 | 37 | return date.strftime("%A, %d %B, %H:%M%p") 38 | 39 | 40 | def french_is_masculine_word(word): 41 | return word[len(word) - 1] not in ['é', 'e'] 42 | 43 | 44 | def starts_with_vowel(word): 45 | return word[0] in ['a', 'e', 'i', 'o', 'u', 'y'] 46 | 47 | 48 | class SentenceGenerator(object): 49 | def __init__(self, locale="en_US"): 50 | """ 51 | :param language: 52 | :type language: Language 53 | """ 54 | self.locale = locale 55 | 56 | def generate_sentence_introduction(self, tone): 57 | """ 58 | 59 | :param tone: 60 | :type tone: SentenceTone 61 | :return: an introduction string 62 | :rtype:basestring 63 | """ 64 | 65 | sentence_beginnings = { 66 | "en_US": { 67 | SentenceTone.POSITIVE: "Yes,", 68 | SentenceTone.NEGATIVE: "No,", 69 | SentenceTone.NEUTRAL: "" 70 | }, 71 | "fr_FR": { 72 | SentenceTone.POSITIVE: "Oui,", 73 | SentenceTone.NEGATIVE: "Non,", 74 | SentenceTone.NEUTRAL: "" 75 | }, 76 | "es_ES": { 77 | SentenceTone.POSITIVE: "Sí,", 78 | SentenceTone.NEGATIVE: "No,", 79 | SentenceTone.NEUTRAL: "" 80 | } 81 | } 82 | 83 | sentence_beginning = sentence_beginnings[self.locale][tone] 84 | return sentence_beginning 85 | 86 | def generate_sentence_locality(self, POI=None, Locality=None, Region=None, Country=None): 87 | """ 88 | :param Locality: 89 | :type Locality:basestring 90 | :param POI: 91 | :type POI:basestring 92 | :param Region: 93 | :type Region:basestring 94 | :param Country: 95 | :type Country:basestring 96 | :return: 97 | :rtype: basestring 98 | """ 99 | if self.locale == "en_US": 100 | if POI or Locality or Region or Country: 101 | locality = filter(lambda x: x is not None, [POI, Locality, Region, Country])[0] 102 | return "in {}".format(locality) 103 | else: 104 | return "" 105 | 106 | if self.locale == "fr_FR": 107 | """ 108 | Country granularity: 109 | - We use "au" for masculine nouns that begins with a consonant 110 | - We use "en" with feminine words and masculine words that start with a vowel 111 | - Careful with the country Malte. Which is an exception 112 | 113 | Region granularity: 114 | We use the "en" for regions 115 | 116 | Locality granularity: 117 | This is used for cities, We use the "à" preposition. 118 | 119 | POIs granularity: 120 | We use the "à" preposition 121 | 122 | """ 123 | 124 | if POI: 125 | return "à {}".format(POI) 126 | 127 | if Locality: 128 | return "à {}".format(Locality) 129 | 130 | if Region: 131 | if french_is_masculine_word(Region) and (not starts_with_vowel(Region)): 132 | return "au {}".format(Region) 133 | else: 134 | return "en {}".format(Region) 135 | if Country: 136 | if french_is_masculine_word(Country) and (not starts_with_vowel(Country)): 137 | return "au {}".format(Country) 138 | else: 139 | return "en {}".format(Country) 140 | 141 | return "" 142 | 143 | if self.locale == "es_ES": 144 | if POI or Locality or Region or Country: 145 | locality = filter(lambda x: x is not None, [POI, Locality, Region, Country])[0] 146 | return "en {}".format(locality) 147 | else: 148 | return "" 149 | 150 | else: 151 | return "" 152 | 153 | def generate_sentence_date(self, date, granularity=0): 154 | """ Convert a date to a string, with an appropriate level of 155 | granularity. 156 | 157 | :param date: A datetime object. 158 | :param granularity: Granularity for the desired textual representation. 159 | 0: precise (date and time are returned) 160 | 1: day (only the week day is returned) 161 | 2: month (only year and month are returned) 162 | 3: year (only year is returned) 163 | :return: A textual representation of the date. 164 | """ 165 | 166 | full_locale = "{}.UTF-8".format(self.locale) 167 | 168 | try: # Careful, this operation is not thread safe ... 169 | locale.setlocale(locale.LC_TIME, full_locale) 170 | except locale.Error: 171 | print "Careful! There was an error while trying to set the locale {}. This means your locale is not properly installed. Please refer to the README for more information.".format(full_locale) 172 | print "Some information displayed might not be formated to your locale" 173 | 174 | return date_to_string(date, granularity) 175 | 176 | def generate_condition_description(self, condition_description): 177 | return condition_description if len(condition_description) > 0 else "" 178 | 179 | def generate_condition_sentence(self, 180 | tone=SentenceTone.POSITIVE, 181 | date=None, granularity=0, 182 | condition_description=None, 183 | POI=None, Locality=None, Region=None, Country=None): 184 | """ 185 | The sentence is generated from those parts : 186 | - introduction (We answer positively or negatively to the user) 187 | - condition (we describe the condition to the user) 188 | - date (when is the condition happening) 189 | - locality (where the condition is happening) 190 | 191 | :param tone: 192 | :type tone: SentenceTone 193 | :param condition_description: 194 | :type condition_description: basestring 195 | :param locality: 196 | :type locality:basestring 197 | :param date: 198 | :type date:datetime 199 | :return: 200 | :rtype: 201 | 202 | """ 203 | introduction = self.generate_sentence_introduction(tone) 204 | 205 | locality = self.generate_sentence_locality(POI, Locality, Region, Country) 206 | 207 | date = self.generate_sentence_date(date, granularity=granularity) 208 | 209 | permutable_parameters = list((locality, date)) 210 | random.shuffle(permutable_parameters) 211 | parameters = (introduction, condition_description) + tuple(permutable_parameters) 212 | 213 | # Formatting 214 | parameters = filter(lambda x: not x is None and len(x) > 0, parameters) 215 | return ("{} " * len(parameters)).format(*parameters) 216 | 217 | def generate_temperature_sentence(self, 218 | temperature="-273.15", 219 | date=None, granularity=0, 220 | POI=None, Locality=None, Region=None, Country=None): 221 | """ 222 | The sentence is generated from those parts : 223 | - the temperature for the date and time 224 | - date (the time when their will be such a temperature ) 225 | - locality (the place their is such a temperature) 226 | 227 | :param temperature 228 | :param locality: 229 | :type locality:basestring 230 | :param date: 231 | :type date:datetime 232 | :return: 233 | :rtype: 234 | 235 | """ 236 | error_sentences = { 237 | "en_US": "I couldn't fetch the right data for the specified place and date", 238 | "fr_FR": "Je n'ai pas pu récupérer les prévisions de température pour cet endroit et ces dates", 239 | "es_ES": "No he podido encontrar información meteorológica para el lugar y la fecha especificados" 240 | } 241 | 242 | if (temperature is None): 243 | return error_sentences[self.locale] 244 | 245 | sentence_introductions = { 246 | "en_US": ["The temperature will be {} degrees"], 247 | "fr_FR": ["La température sera de {} degrés", "Il fera {} degrés"], 248 | "es_ES": ["La temperatura será de {} grados", "Habrá {} grados"] 249 | } 250 | 251 | introduction = random.choice(sentence_introductions[self.locale]).format(temperature) 252 | locality = self.generate_sentence_locality(POI, Locality, Region, Country) 253 | date = self.generate_sentence_date(date) 254 | 255 | permutable_parameters = list((locality, date)) 256 | random.shuffle(permutable_parameters) 257 | parameters = (introduction,) + tuple(permutable_parameters) 258 | return "{} {} {}".format(*parameters) 259 | 260 | def generate_error_sentence(self): 261 | error_sentences = { 262 | "en_US": "An error occured when trying to retrieve the weather, please try again", 263 | "fr_FR": "Désolé, il y a eu une erreur lors de la récupération des données météo. Veuillez réessayer", 264 | "es_ES": "Ha ocurrido un error obteniendo la información climática, por favor inténtalo de nuevo" 265 | } 266 | 267 | return error_sentences[self.locale] 268 | 269 | def generate_api_key_error_sentence(self): 270 | error_sentences = { 271 | "en_US": "The API key you provided is invalid, check your config.ini", 272 | "fr_FR": "La clé API fournie est incorrecte, vérifiez le fichier config.ini", 273 | "es_ES": "La clave de la API es incorrecta, por favor verifica tu fichero config.ini" 274 | } 275 | return error_sentences[self.locale] -------------------------------------------------------------------------------- /snipsowm/snips.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | 4 | 5 | class SnipsWeatherConditions(Enum): 6 | HUMID = 0 7 | BLIZZARD = 1 8 | SNOWFALL = 2 9 | WINDY = 3 10 | CLOUD = 4 11 | RAINY = 5 12 | STORMY = 6 13 | SUN = 7 14 | SNOW = 8 15 | FOG = 9 16 | DEPRESSION = 10 17 | STORM = 11 18 | RAINFALL = 12 19 | SNOWY = 13 20 | SUNNY = 14 21 | RAIN = 15 22 | HAIL = 16 23 | FOGGY = 17 24 | OVERCAST = 18 25 | CLOUDY = 19 26 | HUMIDITY = 20 27 | SNOWSTORM = 21 28 | WIND = 22 29 | TRENCH_COAT = 23 # TODO REMOVE WHEN INTENT 'ITEM' WILL BE INDEPENDENTLY MANAGE 30 | PARKA = 24 31 | CARDIGAN = 25 32 | SUMMER_CLOTHING = 26 33 | GAMP = 27 34 | BROLLY = 28 35 | SUNSHADE = 29 36 | PARASOL = 30 37 | UMBRELLA = 31 38 | OPEN_TOED_SHOES = 32 39 | SHORTS = 33 40 | SKIRT = 34 41 | WARM_JUMPER = 35 42 | WARM_SOCKS = 36 43 | WARM_SWEATER = 37 44 | SCARF = 38 45 | STRAW_HAT = 39 46 | HAT = 40 47 | SUNBLOCK = 41 48 | SUNSCREEN = 42 49 | SUN_CREAM = 43 50 | WOOLEN_SWEATER = 44 51 | WOOLEN_JUMPER = 45 52 | WOOLEN_SOCKS = 46 53 | WOOLEN_TIGHTS = 47 54 | SLEEVELESS_SUNDRESS = 48 55 | SUNDRESS = 49 56 | CHUNKY_SWEATER = 50 57 | SUNGLASSES = 51 58 | RAINCOAT = 52 59 | 60 | 61 | mappings = { 62 | SnipsWeatherConditions.HUMID: { 63 | 'fr_FR': [u'humide'], 64 | 'en_US': [u'humid'], 65 | 'es_ES': [u'húmedo'] 66 | }, 67 | SnipsWeatherConditions.BLIZZARD: { 68 | 'fr_FR': [u'blizzard'], 69 | 'en_US': [u'blizzard'], 70 | 'es_ES': [u'ventisca'] 71 | }, 72 | SnipsWeatherConditions.SNOWFALL: { 73 | 'fr_FR': [u'chutes de neige'], 74 | 'en_US': [u'snowfall'], 75 | 'es_ES': [u'nieve'] 76 | }, 77 | SnipsWeatherConditions.WINDY: { 78 | 'fr_FR': [u'venteux'], 79 | 'en_US': [u'windy'], 80 | 'es_ES': [u'viento'] 81 | }, 82 | SnipsWeatherConditions.CLOUD: { 83 | 'fr_FR': [u'nuage', u'nuages'], 84 | 'en_US': [u'cloud', u'clouds'], 85 | 'es_ES': [u'nuboso'] 86 | }, 87 | SnipsWeatherConditions.RAINY: { 88 | 'fr_FR': [u'pluvieux'], 89 | 'en_US': [u'rainy'], 90 | 'es_ES': [u'lluvioso'] 91 | }, 92 | SnipsWeatherConditions.STORMY: { 93 | 'fr_FR': [u'orageux'], 94 | 'en_US': [u'stormy'], 95 | 'es_ES': [u'tormentoso'] 96 | }, 97 | SnipsWeatherConditions.SUN: { 98 | 'fr_FR': [u'soleil'], 99 | 'en_US': [u'sun'], 100 | 'es_ES': [u'soleado'] 101 | }, 102 | SnipsWeatherConditions.SNOW: { 103 | 'fr_FR': [u'neige', u'neiger', u'neigera'], 104 | 'en_US': [u'snow'], 105 | 'es_ES': [u'nieve'] 106 | }, 107 | SnipsWeatherConditions.FOG: { 108 | 'fr_FR': [u'brouillard'], 109 | 'en_US': [u'fog'], 110 | 'es_ES': [u'niebla'] 111 | }, SnipsWeatherConditions.DEPRESSION: { 112 | 'fr_FR': [u'dépression'], 113 | 'en_US': [u'depression'], 114 | 'es_ES': [u'depresión'] 115 | }, 116 | SnipsWeatherConditions.STORM: { 117 | 'fr_FR': [u'tempête'], 118 | 'en_US': [u'storm'], 119 | 'es_ES': [u'tormenta'] 120 | }, 121 | SnipsWeatherConditions.RAINFALL: { 122 | 'fr_FR': [u'précipitations'], 123 | 'en_US': [u'rainfall'], 124 | 'es_ES': [u'precipitaciones'] 125 | }, 126 | SnipsWeatherConditions.SNOWY: { 127 | 'fr_FR': [u'neigeux'], 128 | 'en_US': [u'snowy'], 129 | 'es_ES': [u'nevada'] 130 | }, 131 | SnipsWeatherConditions.SUNNY: { 132 | 'fr_FR': [u'ensoleillé'], 133 | 'en_US': [u'sunny'], 134 | 'es_ES': [u'soleado'] 135 | }, 136 | SnipsWeatherConditions.RAIN: { 137 | 'fr_FR': [u'pluie'], 138 | 'en_US': [u'rain'], 139 | 'es_ES': [u'lluvia'] 140 | }, 141 | SnipsWeatherConditions.HAIL: { 142 | 'fr_FR': [u'grêle'], 143 | 'en_US': [u'hail'], 144 | 'es_ES': [u'granizo'] 145 | }, 146 | SnipsWeatherConditions.FOGGY: { 147 | 'fr_FR': [u'brumeux'], 148 | 'en_US': [u'foggy'], 149 | 'es_ES': [u'nebuloso'] 150 | }, 151 | SnipsWeatherConditions.OVERCAST: { 152 | 'fr_FR': [u'couvert'], 153 | 'en_US': [u'overcast'], 154 | 'es_ES': [u'cubierto'] 155 | }, 156 | SnipsWeatherConditions.CLOUDY: { 157 | 'fr_FR': [u'nuageux'], 158 | 'en_US': [u'cloudy'], 159 | 'es_ES': [u'nublado'] 160 | }, 161 | SnipsWeatherConditions.HUMIDITY: { 162 | 'fr_FR': [u'humidité'], 163 | 'en_US': [u'humidity'], 164 | 'es_ES': [u'humedad'] 165 | }, SnipsWeatherConditions.SNOWSTORM: { 166 | 'fr_FR': [u'tempête de neige'], 167 | 'en_US': [u'snowstorm'], 168 | 'es_ES': [u'tormenta de nieve'] 169 | }, 170 | SnipsWeatherConditions.WIND: { 171 | 'fr_FR': [u'vent'], 172 | 'en_US': [u'wind'], 173 | 'es_ES': [u'viento'] 174 | }, 175 | SnipsWeatherConditions.TRENCH_COAT: { 176 | 'fr_FR': [u'trench'], 177 | 'en_US': [u'trench coat'], 178 | 'es_ES': [u'gabardina'] 179 | }, 180 | SnipsWeatherConditions.PARKA: { 181 | 'fr_FR': [u'parka'], 182 | 'en_US': [u'parka'], 183 | 'es_ES': [u'anorak'] 184 | }, 185 | SnipsWeatherConditions.CARDIGAN: { 186 | 'fr_FR': [u'cardigan'], 187 | 'en_US': [u'cardigan'], 188 | 'es_ES': [u'chaqueta'] 189 | }, 190 | SnipsWeatherConditions.SUMMER_CLOTHING: { 191 | 'fr_FR': [u'légé'], 192 | 'en_US': [u'summer clothing'], 193 | 'es_ES': [u'ropa de verano'] 194 | }, 195 | SnipsWeatherConditions.GAMP: { 196 | 'fr_FR': [u'parapluie'], 197 | 'en_US': [u'gamp'], 198 | 'es_ES': [u'paraguas'] 199 | }, 200 | SnipsWeatherConditions.BROLLY: { 201 | 'fr_FR': [u'parapluie'], 202 | 'en_US': [u'brolly'], 203 | 'es_ES': [u'paraguas'] 204 | }, 205 | SnipsWeatherConditions.SUNSHADE: { 206 | 'fr_FR': [u'parasol'], 207 | 'en_US': [u'sunshade'], 208 | 'es_ES': [u'sombrilla'] 209 | }, 210 | SnipsWeatherConditions.PARASOL: { 211 | 'fr_FR': [u'parasol'], 212 | 'en_US': [u'parasol'], 213 | 'es_ES': [u'sombrilla'] 214 | }, 215 | SnipsWeatherConditions.UMBRELLA: { 216 | 'fr_FR': [u'parapluie'], 217 | 'en_US': [u'umbrella'], 218 | 'es_ES': [u'paraguas'] 219 | }, SnipsWeatherConditions.OPEN_TOED_SHOES: { 220 | 'fr_FR': [u'sandales'], 221 | 'en_US': [u'open toed shoes'], 222 | 'es_ES': [u'sandalias'] 223 | }, 224 | SnipsWeatherConditions.SHORTS: { 225 | 'fr_FR': [u'short'], 226 | 'en_US': [u'shorts'], 227 | 'es_ES': [u'pantalones cortos'] 228 | }, 229 | SnipsWeatherConditions.SKIRT: { 230 | 'fr_FR': [u'jupe'], 231 | 'en_US': [u'skirt'], 232 | 'es_ES': [u'falda'] 233 | }, 234 | SnipsWeatherConditions.WARM_JUMPER: { 235 | 'fr_FR': [u'jupe courte'], 236 | 'en_US': [u'warm jumper'], 237 | 'es_ES': [u'jersey cálido'] 238 | }, 239 | SnipsWeatherConditions.WARM_SOCKS: { 240 | 'fr_FR': [u'chausettes chaudes'], 241 | 'en_US': [u'warm socks'], 242 | 'es_ES': [u'calcetines cálidos'] 243 | }, 244 | SnipsWeatherConditions.WARM_SWEATER: { 245 | 'fr_FR': [u'pull'], 246 | 'en_US': [u'warm sweater'], 247 | 'es_ES': [u'jersey cálido'] 248 | }, 249 | SnipsWeatherConditions.SCARF: { 250 | 'fr_FR': [u'écharpe'], 251 | 'en_US': [u'scarf'], 252 | 'es_ES': [u'bufanda'] 253 | }, 254 | SnipsWeatherConditions.STRAW_HAT: { 255 | 'fr_FR': [u'chapeau de paille'], 256 | 'en_US': [u'straw hat'], 257 | 'es_ES': [u'sombero de paja'] 258 | }, 259 | SnipsWeatherConditions.HAT: { 260 | 'fr_FR': [u'chapeau'], 261 | 'en_US': [u'hat'], 262 | 'es_ES': [u'sombrero'] 263 | }, 264 | SnipsWeatherConditions.SUNBLOCK: { 265 | 'fr_FR': [u'crème solaire'], 266 | 'en_US': [u'sunblock'], 267 | 'es_ES': [u'crema solar'] 268 | }, 269 | SnipsWeatherConditions.SUNSCREEN: { 270 | 'fr_FR': [u'écran solaire'], 271 | 'en_US': [u'sunscreen'], 272 | 'es_ES': [u'protector solar'] 273 | }, SnipsWeatherConditions.SUN_CREAM: { 274 | 'fr_FR': [u'crème solaire'], 275 | 'en_US': [u'sun cream'], 276 | 'es_ES': [u'crema solar'] 277 | }, 278 | SnipsWeatherConditions.WOOLEN_SWEATER: { 279 | 'fr_FR': [u'pull en laine'], 280 | 'en_US': [u'woolen sweater'], 281 | 'es_ES': [u'jersey de lana'] 282 | }, 283 | SnipsWeatherConditions.WOOLEN_JUMPER: { 284 | 'fr_FR': [u'pull en laine'], 285 | 'en_US': [u'woolen jumper'], 286 | 'es_ES': [u'jersey de lana'] 287 | }, 288 | SnipsWeatherConditions.WOOLEN_TIGHTS: { 289 | 'fr_FR': [u'collants en laine'], 290 | 'en_US': [u'woolen tights'], 291 | 'es_ES': [u'medias de lana'] 292 | }, 293 | SnipsWeatherConditions.SLEEVELESS_SUNDRESS: { 294 | 'fr_FR': [u'robe d\'été'], 295 | 'en_US': [u'sleeveless sundress'], 296 | 'es_ES': [u'vestido sin mangas'] 297 | }, 298 | SnipsWeatherConditions.SUNDRESS: { 299 | 'fr_FR': [u'robe d\'été'], 300 | 'en_US': [u'sundress'], 301 | 'es_ES': [u'vestido de verano'] 302 | }, 303 | SnipsWeatherConditions.CHUNKY_SWEATER: { 304 | 'fr_FR': [u'gros pull'], 305 | 'en_US': [u'chunky sweater'], 306 | 'es_ES': [u'jersey grueso'] 307 | }, 308 | SnipsWeatherConditions.SUNGLASSES: { 309 | 'fr_FR': [u'lunettes de soleil'], 310 | 'en_US': [u'sunglasses'], 311 | 'es_ES': [u'gafas de sol'] 312 | }, 313 | SnipsWeatherConditions.RAINCOAT: { 314 | 'fr_FR': [u'manteau pour la pluie'], 315 | 'en_US': [u'raincoat'], 316 | 'es_ES': [u'impermeable'] 317 | }, 318 | SnipsWeatherConditions.WOOLEN_SOCKS: { 319 | 'fr_FR': [u'chaussettes en laine'], 320 | 'en_US': [u'woolen socks'], 321 | 'es_ES': [u'calcetines de lana'] 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /snipsowm/snipsowm.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ OpenWeatherMap skill for Snips. """ 3 | 4 | import datetime 5 | from feedback.sentence_generator import AnswerSentenceGenerator, ConditionQuerySentenceGenerator, \ 6 | TemperatureQuerySentenceGenerator, SentenceGenerationLocaleException 7 | from provider.owm_provider import OWMWeatherProvider 8 | from provider.weather_provider import WeatherProviderError, WeatherProviderConnectivityError, WeatherProviderInvalidAPIKey 9 | import weather_condition 10 | 11 | 12 | class SnipsOWM: 13 | """ OpenWeatherMap skill for Snips. """ 14 | 15 | def __init__(self, api_key, default_location, locale="en_US"): 16 | """ 17 | :param api_key: OpenWeatherMap API key. 18 | :param default_location: Default location for which to fetch the 19 | weather when none is provided. 20 | :param tts: Optionally, a Text-to-Speech for speaking the weather 21 | results. Such a service should expose a `speak(phrase)` 22 | method. 23 | """ 24 | self.api_key = api_key 25 | self.default_location = default_location 26 | self.locale = locale 27 | 28 | self.provider = OWMWeatherProvider(api_key) 29 | 30 | def speak_temperature(self, locality, date, granularity=0): 31 | """ Tell the temperature at a given locality and datetime. 32 | 33 | :param locality: The locality of the forecast, e.g. 'Paris,fr' or 34 | 'Eiffel Tower' 35 | :type locality: string 36 | 37 | :param date: Time of the forecast, in ISO 8601 format, e.g. 38 | "2017-07-21T10:35:29+00:00" 39 | :type date: datetime 40 | 41 | :return: The temperature at a given locality and datetime. 42 | """ 43 | sentence_generator = TemperatureQuerySentenceGenerator(locale=self.locale) 44 | 45 | if locality is None: 46 | locality = self.default_location 47 | 48 | try: 49 | _, temperature = self.provider.get_weather(locality, datetime=date) 50 | 51 | generated_sentence = sentence_generator.generate_temperature_sentence(temperature=temperature, 52 | date=date, granularity=0, 53 | Locality=locality) 54 | except (WeatherProviderError, WeatherProviderConnectivityError): 55 | generated_sentence = sentence_generator.generate_error_sentence() 56 | except WeatherProviderInvalidAPIKey: 57 | generated_sentence = sentence_generator.generate_api_key_error_sentence() 58 | except SentenceGenerationLocaleException: 59 | generated_sentence = sentence_generator.generate_error_locale_sentence() 60 | 61 | return generated_sentence 62 | 63 | def speak_condition(self, assumed_condition, date, POI=None, Locality=None, Region=None, Country=None, 64 | granularity=0): 65 | """ Speak a response for a given weather condition 66 | at a specified locality and datetime. 67 | If the locality is not specified, use the default location. 68 | 69 | :param assumed_condition: A SnipsWeatherCondition value string 70 | corresponding to a weather condition extracted from the slots. 71 | e.g 'HUMID', 'SUNNY', etc ... 72 | Can be none, if there is no assumption. 73 | :type assumed_condition: basestring 74 | 75 | :param date: datetime of the forecast 76 | :type date: datetime.datetime 77 | 78 | :param POI: Slot value from Snips 79 | :type POI: basestring 80 | 81 | :param Locality: Slot value from Snips 82 | :type Locality: basestring 83 | 84 | :param Region: Slot value from Snips 85 | :type Region: basestring 86 | 87 | :param Country: Slot value from Snips 88 | :type Country: basestring 89 | 90 | :param granularity: Precision with which the date should be described. 91 | :type granularity: int 92 | 93 | :return: A random response for a given weather condition 94 | at a specified locality and datetime. 95 | """ 96 | 97 | # Checking the parameters values 98 | if (POI or Locality or Region or Country): 99 | localities = filter(lambda x: x is not None, [POI, Locality, Region, Country]) 100 | locality = localities[0] 101 | else: 102 | locality = self.default_location 103 | Locality = self.default_location 104 | 105 | # Initializing variables 106 | assumed_condition_group = weather_condition.WeatherConditionDescriptor( 107 | weather_condition.WeatherConditions.UNKNOWN) 108 | tone = AnswerSentenceGenerator.SentenceTone.NEUTRAL 109 | 110 | # We retrieve the condition and the temperature from our weather provider 111 | actual_condition_group = weather_condition.WeatherConditionDescriptor( 112 | weather_condition.WeatherConditions.UNKNOWN) 113 | 114 | sentence_generator = ConditionQuerySentenceGenerator(locale=self.locale) 115 | try: 116 | actual_condition, temperature = self.provider.get_weather(locality, date) 117 | 118 | # We retrieve the weather from our weather provider 119 | actual_condition_group = weather_condition.OWMToWeatherConditionMapper(actual_condition).resolve() 120 | 121 | if assumed_condition: 122 | # We find the category (group) of the received weather description 123 | assumed_condition_group = weather_condition.SnipsToWeatherConditionMapper().fuzzy_matching(self.locale, 124 | assumed_condition).resolve() 125 | 126 | # We check if their is a positive/negative tone to add to the answer 127 | if assumed_condition_group.value != weather_condition.WeatherConditions.UNKNOWN: 128 | tone = AnswerSentenceGenerator.SentenceTone.NEGATIVE if assumed_condition_group.value != actual_condition_group.value else AnswerSentenceGenerator.SentenceTone.POSITIVE 129 | else: 130 | tone = AnswerSentenceGenerator.SentenceTone.NEUTRAL 131 | 132 | # We compose the sentence 133 | generated_sentence = sentence_generator.generate_condition_sentence(tone=tone, 134 | date=date, granularity=granularity, 135 | condition_description=actual_condition_group.describe( 136 | self.locale), 137 | POI=POI, Locality=Locality, 138 | Region=Region, 139 | Country=Country) 140 | 141 | # And finally send it to the TTS if provided 142 | except (WeatherProviderError, WeatherProviderConnectivityError): 143 | generated_sentence = sentence_generator.generate_error_sentence() 144 | except SentenceGenerationLocaleException: 145 | generated_sentence = sentence_generator.generate_error_locale_sentence() 146 | except WeatherProviderInvalidAPIKey: 147 | generated_sentence = sentence_generator.generate_api_key_error_sentence() 148 | 149 | return generated_sentence 150 | 151 | def speak_item(self, item_name, date, POI=None, Locality=None, Region=None, Country=None, 152 | granularity=0): 153 | """ Speak a response for a given a item 154 | at a specified locality and datetime. 155 | If the locality is not specified, use the default location. 156 | 157 | :param item_name: A SnipsWeatherCondition value string 158 | corresponding to a weather condition extracted from the slots. 159 | e.g 'HUMID', 'SUNNY', etc ... 160 | Can be none, if there is no assumption. 161 | :type item_name: basestring 162 | 163 | :param date: datetime of the forecast 164 | :type date: datetime.datetime 165 | 166 | :param POI: Slot value from Snips 167 | :type POI: basestring 168 | 169 | :param Locality: Slot value from Snips 170 | :type Locality: basestring 171 | 172 | :param Region: Slot value from Snips 173 | :type Region: basestring 174 | 175 | :param Country: Slot value from Snips 176 | :type Country: basestring 177 | 178 | :param granularity: Precision with which the date should be described. 179 | :type granularity: int 180 | 181 | :return: A random response for a given weather condition 182 | at a specified locality and datetime. 183 | """ 184 | 185 | # Checking the parameters values 186 | if (POI or Locality or Region or Country): 187 | localities = filter(lambda x: x is not None, [POI, Locality, Region, Country]) 188 | locality = localities[0] 189 | else: 190 | locality = self.default_location 191 | Locality = self.default_location 192 | 193 | # Initializing variables 194 | assumed_condition_group = weather_condition.WeatherConditionDescriptor( 195 | weather_condition.WeatherConditions.UNKNOWN) 196 | tone = AnswerSentenceGenerator.SentenceTone.NEUTRAL 197 | 198 | # We retrieve the condition and the temperature from our weather provider 199 | actual_condition_group = weather_condition.WeatherConditionDescriptor( 200 | weather_condition.WeatherConditions.UNKNOWN) 201 | 202 | sentence_generator = ConditionQuerySentenceGenerator(locale=self.locale) 203 | try: 204 | actual_condition, temperature = self.provider.get_weather(locality, date) 205 | 206 | # We retrieve the weather from our weather provider 207 | actual_condition_group = weather_condition.OWMToWeatherConditionMapper(actual_condition).resolve() 208 | 209 | if item_name: 210 | # We find the category (group) of the received weather description 211 | assumed_condition_group = weather_condition.SnipsToWeatherConditionMapper().fuzzy_matching(self.locale, 212 | item_name).resolve() 213 | 214 | # We check if their is a positive/negative tone to add to the answer 215 | if assumed_condition_group.value != weather_condition.WeatherConditions.UNKNOWN: 216 | tone = AnswerSentenceGenerator.SentenceTone.NEGATIVE if assumed_condition_group.value != actual_condition_group.value else AnswerSentenceGenerator.SentenceTone.POSITIVE 217 | else: 218 | tone = AnswerSentenceGenerator.SentenceTone.NEUTRAL 219 | 220 | # We compose the sentence 221 | generated_sentence = sentence_generator.generate_condition_sentence(tone=tone, 222 | date=date, granularity=granularity, 223 | condition_description=actual_condition_group.describe( 224 | self.locale), 225 | POI=POI, Locality=Locality, 226 | Region=Region, 227 | Country=Country) 228 | 229 | # And finally send it to the TTS if provided 230 | except (WeatherProviderError, WeatherProviderConnectivityError): 231 | generated_sentence = sentence_generator.generate_error_sentence() 232 | except SentenceGenerationLocaleException: 233 | generated_sentence = sentence_generator.generate_error_locale_sentence() 234 | except WeatherProviderInvalidAPIKey: 235 | generated_sentence = sentence_generator.generate_api_key_error_sentence() 236 | 237 | return generated_sentence 238 | -------------------------------------------------------------------------------- /snipsowm/utils.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | def _convert_to_unix_case(text): 4 | result = "" 5 | for i in text: 6 | if i is ' ' or i is '-': 7 | result += '_' 8 | else: 9 | result += i.capitalize() 10 | return result 11 | -------------------------------------------------------------------------------- /snipsowm/wagnerfischerpp.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2013-2016 Kyle Gorman 2 | # 3 | # Permission is hereby granted, free of charge, to any person obtaining a 4 | # copy of this software and associated documentation files (the 5 | # "Software"), to deal in the Software without restriction, including 6 | # without limitation the rights to use, copy, modify, merge, publish, 7 | # distribute, sublicense, and/or sell copies of the Software, and to 8 | # permit persons to whom the Software is furnished to do so, subject to 9 | # the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included 12 | # in all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 15 | # OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 17 | # IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 18 | # CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 19 | # TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 20 | # SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | # 22 | # wagnerfischer.py: efficient computation of Levenshtein distance and 23 | # all optimal alignments with arbitrary edit costs. The algorithm for 24 | # computing the dynamic programming table used has been discovered many 25 | # times, but is described most clearly in: 26 | # 27 | # R.A. Wagner & M.J. Fischer. 1974. The string-to-string correction 28 | # problem. Journal of the ACM, 21(1): 168-173. 29 | # 30 | # Wagner & Fischer also describe an algorithm ("Algorithm Y") to find the 31 | # alignment path (i.e., list of edit operations involved in the optimal 32 | # alignment), but it it is specified such that in fact it only generates 33 | # one such path, whereas many such paths may exist, particularly when 34 | # multiple edit operations have the same cost. For example, when all edit 35 | # operations have the same cost, there are two equal-cost alignments of 36 | # "TGAC" and "GCAC": 37 | # 38 | # TGAC TGxAC 39 | # ss== d=i== 40 | # GCAC xGCAC 41 | # 42 | # However, all such paths can be generated efficiently, as follows. First, 43 | # the dynamic programming table "cells" are defined as tuples of (partial 44 | # cost, set of all operations reaching this cell with minimal cost). As a 45 | # result, the completed table can be thought of as an unweighted, directed 46 | # graph (or FSA). The bottom right cell (the one containing the Levenshtein 47 | # distance) is the start state and the origin as end state. The set of arcs 48 | # are the set of operations in each cell as arcs. (Many of the cells of the 49 | # table, those which are not visited by any optimal alignment, are under 50 | # the graph interpretation unconnected vertices, and can be ignored. Every 51 | # path between the bottom right cell and the origin cell is an optimal 52 | # alignment. These paths can be efficiently enumerated using breadth-first 53 | # traversal. The trick here is that elements in deque must not only contain 54 | # indices but also partial paths. Averaging over all such paths, we can 55 | # come up with an estimate of the number of insertions, deletions, and 56 | # substitutions involved as well; in the example above, we say S = 1 and 57 | # D, I = 0.5. 58 | # 59 | # Thanks to Christoph Weidemann (ctw@cogsci.info), who added support for 60 | # arbitrary cost functions. 61 | 62 | 63 | import collections 64 | import pprint 65 | 66 | 67 | # Default cost functions. 68 | 69 | 70 | def INSERTION(A, cost=1): 71 | return cost 72 | 73 | 74 | def DELETION(A, cost=1): 75 | return cost 76 | 77 | 78 | def SUBSTITUTION(A, B, cost=1): 79 | return cost 80 | 81 | 82 | Trace = collections.namedtuple("Trace", ["cost", "ops"]) 83 | 84 | 85 | class WagnerFischer(object): 86 | 87 | """ 88 | An object representing a (set of) Levenshtein alignments between two 89 | iterable objects (they need not be strings). The cost of the optimal 90 | alignment is scored in `self.cost`, and all Levenshtein alignments can 91 | be generated using self.alignments()`. 92 | 93 | Basic tests: 94 | 95 | >>> WagnerFischer("god", "gawd").cost 96 | 2 97 | >>> WagnerFischer("sitting", "kitten").cost 98 | 3 99 | >>> WagnerFischer("bana", "banananana").cost 100 | 6 101 | >>> WagnerFischer("bana", "bana").cost 102 | 0 103 | >>> WagnerFischer("banana", "angioplastical").cost 104 | 11 105 | >>> WagnerFischer("angioplastical", "banana").cost 106 | 11 107 | >>> WagnerFischer("Saturday", "Sunday").cost 108 | 3 109 | 110 | IDS tests: 111 | 112 | >>> WagnerFischer("doytauvab", "doyvautab").IDS() == {"S": 2.0} 113 | True 114 | >>> WagnerFischer("kitten", "sitting").IDS() == {"I": 1.0, "S": 2.0} 115 | True 116 | 117 | Detect insertion vs. deletion: 118 | 119 | >>> thesmalldog = "the small dog".split() 120 | >>> thebigdog = "the big dog".split() 121 | >>> bigdog = "big dog".split() 122 | >>> sub_inf = lambda A, B: float("inf") 123 | 124 | # Deletion. 125 | >>> wf = WagnerFischer(thebigdog, bigdog, substitution=sub_inf) 126 | >>> wf.IDS() == {"D": 1.0} 127 | True 128 | 129 | # Insertion. 130 | >>> wf = WagnerFischer(bigdog, thebigdog, substitution=sub_inf) 131 | >>> wf.IDS() == {"I": 1.0} 132 | True 133 | 134 | # Neither. 135 | >>> wf = WagnerFischer(thebigdog, thesmalldog, substitution=sub_inf) 136 | >>> wf.IDS() == {"I": 1.0, "D": 1.0} 137 | True 138 | """ 139 | 140 | # Initializes pretty printer (shared across all class instances). 141 | pprinter = pprint.PrettyPrinter(width=75) 142 | 143 | def __init__(self, A, B, insertion=INSERTION, deletion=DELETION, 144 | substitution=SUBSTITUTION): 145 | # Stores cost functions in a dictionary for programmatic access. 146 | self.costs = {"I": insertion, "D": deletion, "S": substitution} 147 | # Initializes table. 148 | self.asz = len(A) 149 | self.bsz = len(B) 150 | self._table = [[None for _ in range(self.bsz + 1)] for 151 | _ in range(self.asz + 1)] 152 | # From now on, all indexing done using self.__getitem__. 153 | ## Fills in edges. 154 | self[0][0] = Trace(0, {"O"}) # Start cell. 155 | for i in range(1, self.asz + 1): 156 | self[i][0] = Trace(self[i - 1][0].cost + self.costs["D"](A[i - 1]), 157 | {"D"}) 158 | for j in range(1, self.bsz + 1): 159 | self[0][j] = Trace(self[0][j - 1].cost + self.costs["I"](B[j - 1]), 160 | {"I"}) 161 | ## Fills in rest. 162 | for i in range(len(A)): 163 | for j in range(len(B)): 164 | # Cleans it up in case there are more than one check for match 165 | # first, as it is always the cheapest option. 166 | if A[i] == B[j]: 167 | self[i + 1][j + 1] = Trace(self[i][j].cost, {"M"}) 168 | # Checks for other types. 169 | else: 170 | costD = self[i][j + 1].cost + self.costs["D"](A[i]) 171 | costI = self[i + 1][j].cost + self.costs["I"](B[j]) 172 | costS = self[i][j].cost + self.costs["S"](A[i], B[j]) 173 | min_val = min(costI, costD, costS) 174 | trace = Trace(min_val, set()) 175 | # Adds _all_ operations matching minimum value. 176 | if costD == min_val: 177 | trace.ops.add("D") 178 | if costI == min_val: 179 | trace.ops.add("I") 180 | if costS == min_val: 181 | trace.ops.add("S") 182 | self[i + 1][j + 1] = trace 183 | # Stores optimum cost as a property. 184 | self.cost = self[-1][-1].cost 185 | 186 | def __repr__(self): 187 | return self.pprinter.pformat(self._table) 188 | 189 | def __iter__(self): 190 | for row in self._table: 191 | yield row 192 | 193 | def __getitem__(self, i): 194 | """ 195 | Returns the i-th row of the table, which is a list and so 196 | can be indexed. Therefore, e.g., self[2][3] == self._table[2][3] 197 | """ 198 | return self._table[i] 199 | 200 | # Stuff for generating alignments. 201 | 202 | def _stepback(self, i, j, trace, path_back): 203 | """ 204 | Given a cell location (i, j) and a Trace object trace, generate 205 | all traces they point back to in the table 206 | """ 207 | for op in trace.ops: 208 | if op == "M": 209 | yield i - 1, j - 1, self[i - 1][j - 1], path_back + ["M"] 210 | elif op == "I": 211 | yield i, j - 1, self[i][j - 1], path_back + ["I"] 212 | elif op == "D": 213 | yield i - 1, j, self[i - 1][j], path_back + ["D"] 214 | elif op == "S": 215 | yield i - 1, j - 1, self[i - 1][j - 1], path_back + ["S"] 216 | elif op == "O": 217 | return # Origin cell, so we"re done. 218 | else: 219 | raise ValueError("Unknown op {!r}".format(op)) 220 | 221 | def alignments(self): 222 | """ 223 | Generate all alignments with optimal-cost via breadth-first 224 | traversal of the graph of all optimal-cost (reverse) paths 225 | implicit in the dynamic programming table 226 | """ 227 | # Each cell of the queue is a tuple of (i, j, trace, path_back) 228 | # where i, j is the current index, trace is the trace object at 229 | # this cell, and path_back is a reversed list of edit operations 230 | # which is initialized as an empty list. 231 | queue = collections.deque(self._stepback(self.asz, self.bsz, 232 | self[-1][-1], [])) 233 | while queue: 234 | (i, j, trace, path_back) = queue.popleft() 235 | if trace.ops == {"O"}: 236 | # We have reached the origin, the end of a reverse path, so 237 | # yield the list of edit operations in reverse. 238 | yield path_back[::-1] 239 | continue 240 | queue.extend(self._stepback(i, j, trace, path_back)) 241 | 242 | def IDS(self): 243 | """ 244 | Estimates insertions, deletions, and substitution _count_ (not 245 | costs). Non-integer values arise when there are multiple possible 246 | alignments with the same cost. 247 | """ 248 | npaths = 0 249 | opcounts = collections.Counter() 250 | for alignment in self.alignments(): 251 | # Counts edit types for this path, ignoring "M" (which is free). 252 | opcounts += collections.Counter(op for op in alignment if op != "M") 253 | npaths += 1 254 | # Averages over all paths. 255 | return collections.Counter({o: c / npaths for (o, c) in 256 | opcounts.items()}) 257 | -------------------------------------------------------------------------------- /snipsowm/weather.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from enum import Enum 3 | 4 | 5 | # These are the reference weather conditions. 6 | class WeatherConditions(Enum): 7 | UNKNOWN = 0 8 | DRIZZLE = 1 9 | RAIN = 2 10 | SNOW = 3 11 | FOG = 4 12 | SUN = 5 13 | CLOUDS = 6 14 | STORM = 7 15 | HUMID = 8 16 | WIND = 9 17 | THUNDERSTORM = 10 18 | -------------------------------------------------------------------------------- /snipsowm/weather_condition.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from wagnerfischerpp import WagnerFischer 3 | from owm import OWMWeatherConditions 4 | from snips import SnipsWeatherConditions, mappings 5 | import random 6 | from utils import _convert_to_unix_case 7 | from weather import WeatherConditions 8 | 9 | 10 | class WeatherConditionDescriptor(object): 11 | """ 12 | A utility class that describes description sentences for WeatherCondition. 13 | """ 14 | 15 | def __init__(self, key): 16 | self.value = key 17 | 18 | descriptions = { 19 | WeatherConditions.THUNDERSTORM: { 20 | "en_US": ["Thunderstorms are expected", "expect thunderstorms"], 21 | "fr_FR": ["de l'orage et des éclair sont prévus"], 22 | "es_ES": ["Se esperan tormentas"], 23 | }, 24 | WeatherConditions.DRIZZLE: { 25 | "en_US": ["drizzle are expected", "expect drizzle"], 26 | "fr_FR": ["prévoir de la bruine"], 27 | "es_ES": ["Se espera granizo"], 28 | }, 29 | WeatherConditions.RAIN: { 30 | "en_US": ["rain is expected", "it's going to be rainy", "expect rain"], 31 | "fr_FR": ["il va pleuvoir", "il pleuvra", "le temps sera pluvieux"], 32 | "es_ES": ["Se esperan lluvias"], 33 | }, 34 | WeatherConditions.SNOW: { 35 | "en_US": ["snow is expected", "it's going to snow", "expect snow"], 36 | "fr_FR": ["il neigera", "il va neiger", "le temps sera neigeux"], 37 | "es_ES": ["Se esperan nevadas"], 38 | }, 39 | WeatherConditions.FOG: { 40 | "en_US": ["fog is expected", "it's going to be foggy", "expect fog"], 41 | "fr_FR": ["Il y aura du brouillard"], 42 | "es_ES": ["Se espera niebla"], 43 | }, 44 | WeatherConditions.SUN: { 45 | "en_US": ["sun is expected", "it's going to be sunny", "the sun will shine"], 46 | "fr_FR": ["le temps sera ensoleillé"], 47 | "es_ES": ["Se espera sol", "El sol brillará"], 48 | }, 49 | WeatherConditions.CLOUDS: { 50 | "en_US": ["it's going to be cloudy", "expect clouds"], 51 | "fr_FR": ["le temps sera nuageux"], 52 | "es_ES": ["Se esperan nubes", "El cielo estará nublado"], 53 | }, 54 | WeatherConditions.STORM: { 55 | "en_US": ["storms are expected", "it's going to be stormy", "expect storms"], 56 | "fr_FR": ["il y aura de l'orage"], 57 | "es_ES": ["Se esperan tormentas"], 58 | }, 59 | WeatherConditions.HUMID: { 60 | "en_US": ["humidity is expected", "it's going to be humid"], 61 | "fr_FR": ["le temps sera humide"], 62 | "es_ES": ["La humedad será elevada"], 63 | }, 64 | WeatherConditions.WIND: { 65 | "en_US": ["wind is expected", "it's going to be windy", "expect wind"], 66 | "fr_FR": ["s'attendre à du vent"], 67 | "es_ES": ["Se espera un tiempo ventoso"], 68 | }, 69 | WeatherConditions.UNKNOWN: { 70 | "en_US": ["I don't know how to describe the weather"], 71 | "fr_FR": ["Je ne peux pas décrire la météo"], 72 | "es_ES": ["No puedo decirte el tiempo que hará"], 73 | }, 74 | } 75 | 76 | def describe(self, locale): 77 | return random.choice(self.descriptions[self.value][locale]) 78 | 79 | 80 | class SnipsToWeatherConditionMapper(object): 81 | mappings = { 82 | SnipsWeatherConditions.HUMID: WeatherConditions.HUMID, 83 | SnipsWeatherConditions.BLIZZARD: WeatherConditions.FOG, 84 | SnipsWeatherConditions.SNOWFALL: WeatherConditions.SNOW, 85 | SnipsWeatherConditions.WINDY: WeatherConditions.WIND, 86 | SnipsWeatherConditions.CLOUD: WeatherConditions.CLOUDS, 87 | SnipsWeatherConditions.RAINY: WeatherConditions.RAIN, 88 | SnipsWeatherConditions.STORMY: WeatherConditions.STORM, 89 | SnipsWeatherConditions.SUN: WeatherConditions.SUN, 90 | SnipsWeatherConditions.SNOW: WeatherConditions.SNOW, 91 | SnipsWeatherConditions.FOG: WeatherConditions.FOG, 92 | SnipsWeatherConditions.DEPRESSION: WeatherConditions.RAIN, 93 | SnipsWeatherConditions.STORM: WeatherConditions.STORM, 94 | SnipsWeatherConditions.RAINFALL: WeatherConditions.RAIN, 95 | SnipsWeatherConditions.SNOWY: WeatherConditions.SNOW, 96 | SnipsWeatherConditions.SUNNY: WeatherConditions.SUN, 97 | SnipsWeatherConditions.RAIN: WeatherConditions.RAIN, 98 | SnipsWeatherConditions.HAIL: WeatherConditions.UNKNOWN, 99 | SnipsWeatherConditions.FOGGY: WeatherConditions.FOG, 100 | SnipsWeatherConditions.OVERCAST: WeatherConditions.CLOUDS, 101 | SnipsWeatherConditions.CLOUDY: WeatherConditions.CLOUDS, 102 | SnipsWeatherConditions.HUMIDITY: WeatherConditions.HUMID, 103 | SnipsWeatherConditions.SNOWSTORM: WeatherConditions.SNOW, 104 | SnipsWeatherConditions.WIND: WeatherConditions.WIND, 105 | SnipsWeatherConditions.TRENCH_COAT: WeatherConditions.RAIN, 106 | # TODO REMOVE WHEN INTENT 'ITEM' WILL BE INDEPENDENTLY MANAGED 107 | SnipsWeatherConditions.PARKA: WeatherConditions.RAIN, 108 | SnipsWeatherConditions.CARDIGAN: WeatherConditions.RAIN, 109 | SnipsWeatherConditions.SUMMER_CLOTHING: WeatherConditions.SUN, 110 | SnipsWeatherConditions.GAMP: WeatherConditions.RAIN, 111 | SnipsWeatherConditions.BROLLY: WeatherConditions.RAIN, 112 | SnipsWeatherConditions.SUNSHADE: WeatherConditions.SUN, 113 | SnipsWeatherConditions.PARASOL: WeatherConditions.SUN, 114 | SnipsWeatherConditions.UMBRELLA: WeatherConditions.RAIN, 115 | SnipsWeatherConditions.OPEN_TOED_SHOES: WeatherConditions.SUN, 116 | SnipsWeatherConditions.SHORTS: WeatherConditions.SUN, 117 | SnipsWeatherConditions.SKIRT: WeatherConditions.SUN, 118 | SnipsWeatherConditions.WARM_JUMPER: WeatherConditions.SNOW, 119 | SnipsWeatherConditions.WARM_SOCKS: WeatherConditions.SNOW, 120 | SnipsWeatherConditions.WARM_SWEATER: WeatherConditions.SNOW, 121 | SnipsWeatherConditions.SCARF: WeatherConditions.SNOW, 122 | SnipsWeatherConditions.STRAW_HAT: WeatherConditions.SUN, 123 | SnipsWeatherConditions.HAT: WeatherConditions.SUN, 124 | SnipsWeatherConditions.SUNBLOCK: WeatherConditions.SUN, 125 | SnipsWeatherConditions.SUNSCREEN: WeatherConditions.SUN, 126 | SnipsWeatherConditions.SUN_CREAM: WeatherConditions.SUN, 127 | SnipsWeatherConditions.WOOLEN_SWEATER: WeatherConditions.SNOW, 128 | SnipsWeatherConditions.WOOLEN_JUMPER: WeatherConditions.SNOW, 129 | SnipsWeatherConditions.WOOLEN_SOCKS: WeatherConditions.SNOW, 130 | SnipsWeatherConditions.WOOLEN_TIGHTS: WeatherConditions.SNOW, 131 | SnipsWeatherConditions.SLEEVELESS_SUNDRESS: WeatherConditions.SUN, 132 | SnipsWeatherConditions.SUNDRESS: WeatherConditions.SUN, 133 | SnipsWeatherConditions.CHUNKY_SWEATER: WeatherConditions.CLOUDS, 134 | SnipsWeatherConditions.SUNGLASSES: WeatherConditions.SUN, 135 | SnipsWeatherConditions.RAINCOAT: WeatherConditions.RAIN 136 | 137 | } 138 | 139 | def __init__(self, key=None): 140 | self.value = None 141 | if key: 142 | if type(key) is SnipsWeatherConditions: 143 | self.value = key 144 | elif type(key) is int: 145 | try: 146 | self.value = SnipsWeatherConditions(key) 147 | except: 148 | pass 149 | elif type(key) is str: 150 | if key in SnipsWeatherConditions.__members__: 151 | self.value = SnipsWeatherConditions[key] 152 | 153 | def fuzzy_matching(self, locale, condition_name): 154 | self.value = SlotValueResolver().fuzzy_match(locale, condition_name) 155 | return self 156 | 157 | def resolve(self): 158 | """ 159 | Resolves a SnipsWeatherCondition to a WeatherCondition 160 | :return: a WeatherCondition 161 | :rtype: WeatherCondition 162 | """ 163 | if self.value is None: return WeatherConditionDescriptor(WeatherConditions.UNKNOWN) 164 | return WeatherConditionDescriptor(self.mappings[self.value]) 165 | 166 | 167 | class OWMToWeatherConditionMapper(object): 168 | mappings = { 169 | OWMWeatherConditions.THUNDERSTORM_WITH_LIGHT_RAIN: WeatherConditions.THUNDERSTORM, 170 | OWMWeatherConditions.THUNDERSTORM_WITH_RAIN: WeatherConditions.THUNDERSTORM, 171 | OWMWeatherConditions.THUNDERSTORM_WITH_HEAVY_RAIN: WeatherConditions.THUNDERSTORM, 172 | OWMWeatherConditions.LIGHT_THUNDERSTORM: WeatherConditions.THUNDERSTORM, 173 | OWMWeatherConditions.THUNDERSTORM: WeatherConditions.THUNDERSTORM, 174 | OWMWeatherConditions.HEAVY_THUNDERSTORM: WeatherConditions.THUNDERSTORM, 175 | OWMWeatherConditions.RAGGED_THUNDERSTORM: WeatherConditions.THUNDERSTORM, 176 | OWMWeatherConditions.THUNDERSTORM_WITH_LIGHT_DRIZZLE: WeatherConditions.THUNDERSTORM, 177 | OWMWeatherConditions.THUNDERSTORM_WITH_DRIZZLE: WeatherConditions.THUNDERSTORM, 178 | OWMWeatherConditions.THUNDERSTORM_WITH_HEAVY_DRIZZLE: WeatherConditions.THUNDERSTORM, 179 | OWMWeatherConditions.LIGHT_INTENSITY_DRIZZLE: WeatherConditions.DRIZZLE, 180 | OWMWeatherConditions.DRIZZLE: WeatherConditions.DRIZZLE, 181 | OWMWeatherConditions.HEAVY_INTENSITY_DRIZZLE: WeatherConditions.DRIZZLE, 182 | OWMWeatherConditions.LIGHT_INTENSITY_DRIZZLE_RAIN: WeatherConditions.DRIZZLE, 183 | OWMWeatherConditions.DRIZZLE_RAIN: WeatherConditions.DRIZZLE, 184 | OWMWeatherConditions.HEAVY_INTENSITY_DRIZZLE_RAIN: WeatherConditions.DRIZZLE, 185 | OWMWeatherConditions.SHOWER_RAIN_AND_DRIZZLE: WeatherConditions.DRIZZLE, 186 | OWMWeatherConditions.HEAVY_SHOWER_RAIN_AND_DRIZZLE: WeatherConditions.DRIZZLE, 187 | OWMWeatherConditions.SHOWER_DRIZZLE: WeatherConditions.DRIZZLE, 188 | OWMWeatherConditions.LIGHT_RAIN: WeatherConditions.RAIN, 189 | OWMWeatherConditions.MODERATE_RAIN: WeatherConditions.RAIN, 190 | OWMWeatherConditions.HEAVY_INTENSITY_RAIN: WeatherConditions.RAIN, 191 | OWMWeatherConditions.VERY_HEAVY_RAIN: WeatherConditions.RAIN, 192 | OWMWeatherConditions.EXTREME_RAIN: WeatherConditions.RAIN, 193 | OWMWeatherConditions.FREEZING_RAIN: WeatherConditions.RAIN, 194 | OWMWeatherConditions.LIGHT_INTENSITY_SHOWER_RAIN: WeatherConditions.RAIN, 195 | OWMWeatherConditions.SHOWER_RAIN: WeatherConditions.RAIN, 196 | OWMWeatherConditions.HEAVY_INTENSITY_SHOWER_RAIN: WeatherConditions.RAIN, 197 | OWMWeatherConditions.RAGGED_SHOWER_RAIN: WeatherConditions.RAIN, 198 | OWMWeatherConditions.LIGHT_SNOW: WeatherConditions.SNOW, 199 | OWMWeatherConditions.SNOW: WeatherConditions.SNOW, 200 | OWMWeatherConditions.HEAVY_SNOW: WeatherConditions.SNOW, 201 | OWMWeatherConditions.SLEET: WeatherConditions.SNOW, 202 | OWMWeatherConditions.SHOWER_SLEET: WeatherConditions.RAIN, 203 | OWMWeatherConditions.LIGHT_RAIN_AND_SNOW: WeatherConditions.RAIN, 204 | OWMWeatherConditions.RAIN_AND_SNOW: WeatherConditions.RAIN, 205 | OWMWeatherConditions.LIGHT_SHOWER_SNOW: WeatherConditions.SNOW, 206 | OWMWeatherConditions.SHOWER_SNOW: WeatherConditions.SNOW, 207 | OWMWeatherConditions.HEAVY_SHOWER_SNOW: WeatherConditions.SNOW, 208 | OWMWeatherConditions.MIST: WeatherConditions.FOG, 209 | OWMWeatherConditions.SMOKE: WeatherConditions.FOG, 210 | OWMWeatherConditions.HAZE: WeatherConditions.FOG, 211 | OWMWeatherConditions.SAND_DUST_WHIRLS: WeatherConditions.UNKNOWN, 212 | OWMWeatherConditions.FOG: WeatherConditions.FOG, 213 | OWMWeatherConditions.SAND: WeatherConditions.UNKNOWN, 214 | OWMWeatherConditions.DUST: WeatherConditions.UNKNOWN, 215 | OWMWeatherConditions.VOLCANIC_ASH: WeatherConditions.UNKNOWN, 216 | OWMWeatherConditions.SQUALLS: WeatherConditions.UNKNOWN, 217 | OWMWeatherConditions.TORNAD: WeatherConditions.UNKNOWN, 218 | OWMWeatherConditions.FEW_CLOUDS: WeatherConditions.CLOUDS, 219 | OWMWeatherConditions.SCATTERED_CLOUDS: WeatherConditions.CLOUDS, 220 | OWMWeatherConditions.BROKEN_CLOUDS: WeatherConditions.CLOUDS, 221 | OWMWeatherConditions.OVERCAST_CLOUDS: WeatherConditions.CLOUDS, 222 | OWMWeatherConditions.CLEAR_SKY: WeatherConditions.SUN, 223 | OWMWeatherConditions.TORNADO: WeatherConditions.STORM, 224 | OWMWeatherConditions.TROPICAL_STORM: WeatherConditions.STORM, 225 | OWMWeatherConditions.HURRICANE: WeatherConditions.STORM, 226 | OWMWeatherConditions.COLD: WeatherConditions.UNKNOWN, 227 | OWMWeatherConditions.HOT: WeatherConditions.UNKNOWN, 228 | OWMWeatherConditions.WINDY: WeatherConditions.UNKNOWN, 229 | OWMWeatherConditions.HAIL: WeatherConditions.UNKNOWN 230 | } 231 | 232 | def __init__(self, key): 233 | self.value = None 234 | if type(key) is OWMWeatherConditions: 235 | self.value = key 236 | elif type(key) is int: 237 | try: 238 | self.value = OWMWeatherConditions(key) 239 | except: 240 | pass 241 | elif type(key) is str: 242 | key = _convert_to_unix_case(key) 243 | if key in OWMWeatherConditions.__members__: 244 | self.value = OWMWeatherConditions[key] 245 | 246 | def resolve(self): 247 | if self.value is None: return WeatherConditionDescriptor(WeatherConditions.UNKNOWN) 248 | return WeatherConditionDescriptor(self.mappings[self.value]) 249 | 250 | 251 | class SlotValueResolver(object): 252 | def normalize_input(self, input_string): 253 | return input_string.lower().strip() 254 | 255 | def fuzzy_match(self, locale, condition_name): 256 | condition_name = self.normalize_input(condition_name) 257 | conditions_candidates = self.get_condition_candidates(locale, condition_name) 258 | 259 | sorted_candidates = sorted(conditions_candidates.items(), 260 | cmp=lambda x, y: WagnerFischer(condition_name, x[1]).cost - WagnerFischer(condition_name, y[1]).cost) 261 | return sorted_candidates[0][0] 262 | 263 | def get_condition_candidates(self, locale, condition_name): 264 | return {condition: min(mappings[condition][locale], key=lambda s: WagnerFischer(condition_name, s).cost) for 265 | condition in list(SnipsWeatherConditions)} 266 | 267 | 268 | if __name__ == "__main__": 269 | print SlotValueResolver().fuzzy_match("fr_FR", u'HUMID') -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/tests/__init__.py -------------------------------------------------------------------------------- /tests/feedback_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/tests/feedback_test/__init__.py -------------------------------------------------------------------------------- /tests/feedback_test/sentence_generator_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import datetime 4 | from snipsowm.feedback import sentence_generator 5 | 6 | 7 | class TestSentenceGenerator(TestCase): 8 | def setUp(self): 9 | pass 10 | 11 | def test_conditionQuerySentenceGenerator(self): 12 | pass 13 | 14 | 15 | class TestAnswerSentenceGenerator(TestCase): 16 | def setUp(self): 17 | self.generator = sentence_generator.AnswerSentenceGenerator(locale="random") 18 | 19 | def test_sentence_generation_locality_empty(self): 20 | self.assertEquals(len(self.generator.generate_sentence_locality()), 0) 21 | 22 | 23 | class TestEnglishAnswerSentenceGenerator(TestCase): 24 | def setUp(self): 25 | self.generator = sentence_generator.AnswerSentenceGenerator() 26 | 27 | def test_sentence_generation_locality_empty(self): 28 | self.assertEquals(len(self.generator.generate_sentence_locality()), 0) 29 | 30 | class TestFrenchAnswerSentenceGenerator(TestCase): 31 | def setUp(self): 32 | self.generator = sentence_generator.AnswerSentenceGenerator(locale="fr_FR") 33 | 34 | def test_sentence_generation_locality_empty(self): 35 | self.assertEquals(len(self.generator.generate_sentence_locality()), 0) -------------------------------------------------------------------------------- /tests/provider_test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/snipsco/snips-skill-owm/ab83358c7c9410c6fb10ce829621f14b794444fd/tests/provider_test/__init__.py -------------------------------------------------------------------------------- /tests/provider_test/owm_provider_test.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | import datetime 4 | from snipsowm.provider import owm_provider 5 | 6 | 7 | class TestGetWeather(TestCase): 8 | def setUp(self): 9 | self.api_key = "dummykey" 10 | self.provider = owm_provider.OWMWeatherProvider(self.api_key) 11 | 12 | def test_get_weather_owm_forecast_limit_low(self): 13 | now_date = datetime.datetime.now() 14 | now_date_minus_1_day = now_date + datetime.timedelta(days=-1) 15 | self.assertRaises(owm_provider.OpenWeatherMapMaxDaysForecastError, self.provider.check_owm_forecast_limit, 16 | now_date_minus_1_day) 17 | 18 | def test_get_weather_owm_forecast_limit_high(self): 19 | now_date = datetime.datetime.now() 20 | now_date_minus_15_day = now_date + datetime.timedelta( 21 | days=owm_provider.OWMWeatherProvider.OWM_MAX_FORECAST_DAYS + 1) 22 | self.assertRaises(owm_provider.OpenWeatherMapMaxDaysForecastError, self.provider.check_owm_forecast_limit, 23 | now_date_minus_15_day) 24 | 25 | -------------------------------------------------------------------------------- /tests/test_skill.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import datetime 4 | from unittest import TestCase 5 | 6 | from snipsowm.snipsowm import SnipsOWM 7 | from snipsowm.sentence_generator import SentenceGenerator 8 | 9 | DUMMY_API_KEY = "dummykey" 10 | DUMMY_LOCATION = "DUMMY LOCATION" 11 | 12 | 13 | class EnglishTestTemperature(TestCase): 14 | 15 | def setUp(self): 16 | self.api_key = DUMMY_API_KEY 17 | self.default_location = DUMMY_LOCATION 18 | 19 | def test_skill_speak_temperature_wrong_api_key(self): 20 | skill = SnipsOWM(self.api_key, self.default_location, locale="en_US") 21 | sentence = skill.speak_temperature("Paris", datetime.datetime.now() + datetime.timedelta(hours=+2)) 22 | self.assertEquals(sentence, SentenceGenerator().generate_api_key_error_sentence(), True) 23 | 24 | 25 | class EnglishTestCondition(TestCase): 26 | 27 | def setUp(self): 28 | self.api_key = DUMMY_API_KEY 29 | self.default_location = DUMMY_LOCATION 30 | 31 | def test_skill_speak_condition_wrong_api_key(self): 32 | skill = SnipsOWM(self.api_key, self.default_location, locale="en_US") 33 | sentence = skill.speak_temperature("Paris", datetime.datetime.now() + datetime.timedelta(hours=+2)) 34 | self.assertEquals(sentence, SentenceGenerator().generate_api_key_error_sentence(), True) 35 | 36 | 37 | --------------------------------------------------------------------------------