diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..c7b36f6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,17 @@ +# ~~ Generated by projen. To modify, edit .projenrc.json and run "npx projen". + +/.gitattributes linguist-generated +/.github/workflows/license_tests.yml linguist-generated +/.github/workflows/propose_release.yml linguist-generated +/.github/workflows/publish_alpha.yml linguist-generated +/.github/workflows/publish_release.yml linguist-generated +/.github/workflows/pull-request-lint.yml linguist-generated +/.github/workflows/skill_tests.yml linguist-generated +/.github/workflows/update_skill_json.yml linguist-generated +/.gitignore linguist-generated +/.projen/** linguist-generated +/.projen/deps.json linguist-generated +/.projen/files.json linguist-generated +/.projen/tasks.json linguist-generated +/setup.py linguist-generated +/version.py linguist-generated \ No newline at end of file diff --git a/.github/workflows/auto_translate.yml b/.github/workflows/auto_translate.yml new file mode 100644 index 0000000..007b286 --- /dev/null +++ b/.github/workflows/auto_translate.yml @@ -0,0 +1,12 @@ +name: Auto translate +on: + workflow_dispatch: + +jobs: + autotranslate: + uses: openvoiceos/.github/.github/workflows/auto_translate.yml@feat/shared_actions1 + with: + branch: ${{ github.event.inputs.branch }} + action_branch: feat/shared_actions1 + python_version: "3.10" + locale_folder: locale diff --git a/.github/workflows/pull-request-lint.yml b/.github/workflows/pull-request-lint.yml new file mode 100644 index 0000000..7791784 --- /dev/null +++ b/.github/workflows/pull-request-lint.yml @@ -0,0 +1,30 @@ +# ~~ Generated by projen. To modify, edit .projenrc.json and run "npx projen". + +name: pull-request-lint +on: + pull_request_target: + types: + - labeled + - opened + - synchronize + - reopened + - ready_for_review + - edited +jobs: + validate: + name: Validate PR title + runs-on: ubuntu-latest + permissions: + pull-requests: write + steps: + - uses: amannn/action-semantic-pull-request@v5.0.2 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: |- + feat + fix + chore + docs + requireScope: false + githubBaseUrl: ${{ github.api_url }} diff --git a/.github/workflows/skill_tests.yml b/.github/workflows/skill_tests.yml new file mode 100644 index 0000000..9819ad2 --- /dev/null +++ b/.github/workflows/skill_tests.yml @@ -0,0 +1,35 @@ +# ~~ Generated by projen. To modify, edit .projenrc.json and run "npx projen". + +name: skill_tests +on: + pull_request: {} + workflow_dispatch: {} +jobs: + build-tests: + uses: openvoiceos/.github/.github/workflows/python_build_tests.yml@feat/shared_actions1 + with: + python_matrix: '[3.8, 3.9, "3.10", "3.11"]' + unit-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python: [3.8, 3.9, "3.10", "3.11"] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python }} + - name: Install dependencies + run: pip install ".[test]" pytest-cov + - name: Run tests + run: pytest --cov=. + skill_intent_tests: + uses: neongeckocom/.github/.github/workflows/skill_test_intents.yml@master + with: + test_padacioso: false + neon_versions: "[ 3.8, 3.9, '3.10', '3.11' ]" + ovos_versions: "[ 3.8, 3.9, '3.10', '3.11' ]" + skill_resource_tests: + uses: neongeckocom/.github/.github/workflows/skill_test_resources.yml@master + with: + skill_entrypoint: skill-meal-plan.mikejgray diff --git a/.github/workflows/update_skill_json.yml b/.github/workflows/update_skill_json.yml new file mode 100644 index 0000000..0392efc --- /dev/null +++ b/.github/workflows/update_skill_json.yml @@ -0,0 +1,11 @@ +# ~~ Generated by projen. To modify, edit .projenrc.json and run "npx projen". + +name: update_skill_json +on: + push: {} +jobs: + update-skill-json: + name: update_skill_json + permissions: + contents: write + uses: neongeckocom/.github/.github/workflows/skill_update_json_spec.yml@master diff --git a/.gitignore b/.gitignore index 322e3b2..66eefa7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,92 @@ +# ~~ Generated by projen. To modify, edit .projenrc.json and run "npx projen". +node_modules/ +!/.gitattributes +!/.projen/tasks.json +!/.projen/deps.json +!/.projen/files.json +!/.github/workflows/pull-request-lint.yml +.DS_Store +node_modules __pycache__/ -*.qmlc -settings.json - -venv/ \ No newline at end of file +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST +*.manifest +*.spec +pip-log.txt +pip-delete-this-directory.txt +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ +*.mo +*.pot +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +instance/ +.webassets-cache +.scrapy +docs/_build/ +.pybuilder/ +target/ +.ipynb_checkpoints +profile_default/ +ipython_config.py +__pypackages__/ +celerybeat-schedule +celerybeat.pid +*.sage.py +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ +.spyderproject +.spyproject +.ropeproject +/site +.mypy_cache/ +.dmypy.json +dmypy.json +.pyre/ +.pytype/ +cython_debug/ +!/setup.py +!/version.py +!/.github/workflows/license_tests.yml +!/.github/workflows/propose_release.yml +!/.github/workflows/publish_alpha.yml +!/.github/workflows/publish_release.yml +!/.github/workflows/skill_tests.yml +!/.github/workflows/update_skill_json.yml diff --git a/.projen/files.json b/.projen/files.json new file mode 100644 index 0000000..fbd702a --- /dev/null +++ b/.projen/files.json @@ -0,0 +1,19 @@ +{ + "files": [ + ".gitattributes", + ".github/workflows/license_tests.yml", + ".github/workflows/propose_release.yml", + ".github/workflows/publish_alpha.yml", + ".github/workflows/publish_release.yml", + ".github/workflows/pull-request-lint.yml", + ".github/workflows/skill_tests.yml", + ".github/workflows/update_skill_json.yml", + ".gitignore", + ".projen/deps.json", + ".projen/files.json", + ".projen/tasks.json", + "setup.py", + "version.py" + ], + "//": "~~ Generated by projen. To modify, edit .projenrc.json and run \"npx projen\"." +} diff --git a/.projen/tasks.json b/.projen/tasks.json new file mode 100644 index 0000000..eeb0ba1 --- /dev/null +++ b/.projen/tasks.json @@ -0,0 +1,105 @@ +{ + "tasks": { + "build": { + "name": "build", + "description": "Full release build", + "steps": [ + { + "spawn": "default" + }, + { + "spawn": "pre-compile" + }, + { + "spawn": "compile" + }, + { + "spawn": "post-compile" + }, + { + "spawn": "test" + }, + { + "spawn": "package" + } + ] + }, + "clobber": { + "name": "clobber", + "description": "hard resets to HEAD of origin and cleans the local repo", + "env": { + "BRANCH": "$(git branch --show-current)" + }, + "steps": [ + { + "exec": "git checkout -b scratch", + "name": "save current HEAD in \"scratch\" branch" + }, + { + "exec": "git checkout $BRANCH" + }, + { + "exec": "git fetch origin", + "name": "fetch latest changes from origin" + }, + { + "exec": "git reset --hard origin/$BRANCH", + "name": "hard reset to origin commit" + }, + { + "exec": "git clean -fdx", + "name": "clean all untracked files" + }, + { + "say": "ready to rock! (unpushed commits are under the \"scratch\" branch)" + } + ], + "condition": "git diff --exit-code > /dev/null" + }, + "compile": { + "name": "compile", + "description": "Only compile" + }, + "default": { + "name": "default", + "description": "Synthesize project files", + "env": { + "FILENAME": ".projenrc.json" + }, + "steps": [ + { + "builtin": "run-projenrc-json" + } + ] + }, + "eject": { + "name": "eject", + "description": "Remove projen from the project", + "env": { + "PROJEN_EJECTING": "true" + }, + "steps": [ + { + "spawn": "default" + } + ] + }, + "package": { + "name": "package", + "description": "Creates the distribution package" + }, + "post-compile": { + "name": "post-compile", + "description": "Runs after successful compilation" + }, + "pre-compile": { + "name": "pre-compile", + "description": "Prepare the project for compilation" + }, + "test": { + "name": "test", + "description": "Run tests" + } + }, + "//": "~~ Generated by projen. To modify, edit .projenrc.json and run \"npx projen\"." +} diff --git a/.projenrc.json b/.projenrc.json new file mode 100644 index 0000000..8cd6e21 --- /dev/null +++ b/.projenrc.json @@ -0,0 +1,12 @@ +{ + "type": "@mikejgray/ovos-skill-projen.OVOSSkillProject", + "name": "meal-plan-skill", + "retrofit": true, + "author": "Mike Gray", + "authorAddress": "mike@graywind.org", + "authorHandle": "mikejgray", + "repositoryUrl": "https://github.com/mikejgray/skill-meal-plan", + "packageDir": ".", + "skillClass": "MealPlanSkill", + "license": "Apache-2.0" +} \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..7dc91c9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2024 [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/LICENSE.md b/LICENSE.md deleted file mode 100644 index 45ee739..0000000 --- a/LICENSE.md +++ /dev/null @@ -1,20 +0,0 @@ -Copyright (C) 2012 Yoshimasa Niwa - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md index 082fbfa..ab4d945 100644 --- a/README.md +++ b/README.md @@ -8,9 +8,9 @@ Suggests a meal for you to make, based on a short default list, but you can mana ## Examples -- "What should i make for dinner?" -- "What should i eat?" -- "What should i eat tonight?" +- "What should I make for dinner?" +- "What should I eat?" +- "What should I eat tonight?" - "I'm hungry" - "What's for dinner?" @@ -25,8 +25,8 @@ Productivity ## Tags -#Food meal planning -#Food -#Meal -#Planning -#Meal planning +Food meal planning +Food +Meal +Planning +Meal planning diff --git a/__init__.py b/__init__.py index 5ddd6f2..4a53f4d 100644 --- a/__init__.py +++ b/__init__.py @@ -1,88 +1,99 @@ -from json import dump, loads +# pylint: disable=missing-module-docstring,attribute-defined-outside-init,broad-exception-caught,invalid-name from random import choice -from typing import Dict, List -from mycroft import MycroftSkill, intent_file_handler -from mycroft.util.parse import match_one +from ovos_bus_client.message import Message +from ovos_utils.parse import match_one +from ovos_workshop.decorators import intent_handler +from ovos_workshop.skills import OVOSSkill -INITIAL_MEALS = {"meals": ["Spaghetti and meatballs", "Toasted sandwiches and tomato soup", "Chicken noodle soup"]} +INITIAL_MEALS = ( + "Spaghetti and meatballs,Toasted sandwiches and tomato soup," + "Chicken noodle soup,Peanut butter and jelly sandwiches" +) -class MealPlan(MycroftSkill): - def __init__(self): - MycroftSkill.__init__(self) - self.meals_location = "meals.json" - if not self.file_system.exists(self.meals_location): - with self.file_system.open(self.meals_location, "w") as f: - dump(INITIAL_MEALS, f) +class MealPlanSkill(OVOSSkill): + """A skill to help plan meals.""" - def initialize(self): - self.meals_location = "meals.json" - self.meals = self._get_meals().get("meals") - self._save_meals() + def __init__(self, *args, bus=None, skill_id="", **kwargs): + OVOSSkill.__init__(self, *args, bus=bus, skill_id=skill_id, **kwargs) - def _get_meals(self) -> Dict[str, List[str]]: - """Reads in meals from file in skill directory.""" - with self.file_system.open(self.meals_location, "r") as file: - meals = loads(file.read()) + @property + def _core_lang(self): + """Backwards compatibility for older versions.""" + return self.core_lang + + @property + def _secondary_langs(self): + """Backwards compatibility for older versions.""" + return self.secondary_langs + + @property + def _native_langs(self): + """Backwards compatibility for older versions.""" + return self.native_langs + + @property + def meals(self): + """Get the list of meals from the settings file. Comma-separated string.""" + meals = self.settings.get("meals", INITIAL_MEALS) + meals = meals.replace(", ", ",").replace(" ,", ",") return meals - def _save_meals(self) -> None: - """Saves instantiated meals to file in skill directory.""" - with self.file_system.open(self.meals_location, "w") as f: - dump({"meals": self.meals}, f) - self.log.info(f"Saved meals to {self.meals_location}") + @meals.setter + def meals(self, value): + self.settings["meals"] = value - @intent_file_handler("plan.meal.intent") - def handle_plan_meal(self): + def _remove_meal(self, meal: str) -> str: + """Remove a meal from our list of meals.""" + meals = self.meals.split(",") + meals.remove(meal) + return ",".join(meals) + + @intent_handler("plan.meal.intent") + def handle_plan_meal(self, _: Message): """Handler for initial intent.""" - self.meals = self._get_meals().get("meals") - self.speak_dialog("plan.meal", data={"meal": choice(self.meals)}) + self.speak_dialog("plan.meal", data={"meal": choice(self.meals.split(","))}) - @intent_file_handler("add.meal.intent") - def handle_add_meal(self): + @intent_handler("add.meal.intent") + def handle_add_meal(self, _: Message): """Wait for a response and add it to meals.json""" new_meal = self.get_response("add.meal") try: self.log.info(f"Adding a new meal: {new_meal}") if new_meal: - self.meals.append(new_meal) - self._save_meals() - self.speak(f"Okay, I've added {new_meal} to your list of meals. Yum!") + self.meals = f"{self.meals},{new_meal}" + self.speak_dialog("meal.added") except Exception as err: self.log.exception(err) - self.speak("I wasn't able to add that meal. I'm sorry.") + self.speak_dialog("failed.to.add.meal") - @intent_file_handler("remove.meal.intent") - def handle_remove_meal(self): + @intent_handler("remove.meal.intent") + def handle_remove_meal(self, _: Message): """Handler for removing a meal from our options.""" meal_to_remove = self.get_response("remove.meal") try: best_guess = match_one(meal_to_remove, self.meals)[0] self.log.info(f"Confirming we should remove {best_guess}") - confirm = self.ask_yesno(f"Just to confirm, we're removing {best_guess}, right?") + confirm = self.ask_yesno("confirm.remove.meal", {"meal": best_guess}) if confirm == "yes": - self.meals.remove(best_guess) - self._save_meals() - self.speak("Ok, I won't recommend that anymore.") + self.meals = self._remove_meal(best_guess) + self.speak_dialog("meal.removed") else: self.acknowledge() except Exception as err: self.log.exception(err) - self.speak("I couldn't remove that meal. I'm sorry.") + self.speak_dialog("failed.to.remove.meal") - @intent_file_handler("list.meal.intent") - def handle_list_meals(self): - self.meals = self._get_meals().get("meals") + @intent_handler("list.meal.intent") + def handle_list_meals(self, _: Message): + """List all the meals we have. If there are more than 15, ask for confirmation.""" num_meals = len(self.meals) if num_meals > 15: - confirm = self.ask_yesno(f"Are you sure? You have {num_meals} meals listed. This may take some time.") + confirm = self.ask_yesno("confirm.list.meals", {"num_meals": num_meals}) if confirm == "no": - self.speak("Okay, I won't bore you.") + self.speak_dialog("skip.list.meals") return - self.speak("Okay, here are all your meal options:") - self.speak(", ".join(self.meals)) - - -def create_skill(): - return MealPlan() + self.speak_dialog( + "list.meals.dialog", {"meals": ", ".join(self.meals.split(","))} + ) diff --git a/locale/en-us/add.meal.dialog b/locale/en-us/dialog/add.meal.dialog similarity index 100% rename from locale/en-us/add.meal.dialog rename to locale/en-us/dialog/add.meal.dialog diff --git a/locale/en-us/dialog/confirm.list.meals.dialog b/locale/en-us/dialog/confirm.list.meals.dialog new file mode 100644 index 0000000..f1ea178 --- /dev/null +++ b/locale/en-us/dialog/confirm.list.meals.dialog @@ -0,0 +1 @@ +Are you sure? You have {num_meals} meals listed. This may take some time. \ No newline at end of file diff --git a/locale/en-us/dialog/confirm.remove.meal.dialog b/locale/en-us/dialog/confirm.remove.meal.dialog new file mode 100644 index 0000000..bab3ccb --- /dev/null +++ b/locale/en-us/dialog/confirm.remove.meal.dialog @@ -0,0 +1 @@ +Just to confirm, we're removing {meal}, right? \ No newline at end of file diff --git a/locale/en-us/dialog/failed.to.add.meal.dialog b/locale/en-us/dialog/failed.to.add.meal.dialog new file mode 100644 index 0000000..e28c953 --- /dev/null +++ b/locale/en-us/dialog/failed.to.add.meal.dialog @@ -0,0 +1 @@ +I wasn't able to add that meal. I'm sorry. \ No newline at end of file diff --git a/locale/en-us/dialog/failed.to.remove.meal.dialog b/locale/en-us/dialog/failed.to.remove.meal.dialog new file mode 100644 index 0000000..9f42cc4 --- /dev/null +++ b/locale/en-us/dialog/failed.to.remove.meal.dialog @@ -0,0 +1 @@ +I couldn't remove that meal. I'm sorry. \ No newline at end of file diff --git a/locale/en-us/dialog/list.meals.dialog b/locale/en-us/dialog/list.meals.dialog new file mode 100644 index 0000000..0c7a605 --- /dev/null +++ b/locale/en-us/dialog/list.meals.dialog @@ -0,0 +1 @@ +Okay, here are all your meal options: {meals} \ No newline at end of file diff --git a/locale/en-us/dialog/meal.added.dialog b/locale/en-us/dialog/meal.added.dialog new file mode 100644 index 0000000..6a290e5 --- /dev/null +++ b/locale/en-us/dialog/meal.added.dialog @@ -0,0 +1 @@ +Okay, I've added {new_meal} to your list of meals. Yum! \ No newline at end of file diff --git a/locale/en-us/dialog/meal.removed.dialog b/locale/en-us/dialog/meal.removed.dialog new file mode 100644 index 0000000..6557787 --- /dev/null +++ b/locale/en-us/dialog/meal.removed.dialog @@ -0,0 +1 @@ +Ok, I won't recommend that anymore. \ No newline at end of file diff --git a/locale/en-us/plan.meal.dialog b/locale/en-us/dialog/plan.meal.dialog similarity index 100% rename from locale/en-us/plan.meal.dialog rename to locale/en-us/dialog/plan.meal.dialog diff --git a/locale/en-us/remove.meal.dialog b/locale/en-us/dialog/remove.meal.dialog similarity index 100% rename from locale/en-us/remove.meal.dialog rename to locale/en-us/dialog/remove.meal.dialog diff --git a/locale/en-us/dialog/skip.list.meals.dialog b/locale/en-us/dialog/skip.list.meals.dialog new file mode 100644 index 0000000..76900ca --- /dev/null +++ b/locale/en-us/dialog/skip.list.meals.dialog @@ -0,0 +1 @@ +Okay, I won't bore you. \ No newline at end of file diff --git a/locale/en-us/add.meal.intent b/locale/en-us/intents/add.meal.intent similarity index 100% rename from locale/en-us/add.meal.intent rename to locale/en-us/intents/add.meal.intent diff --git a/locale/en-us/list.meal.intent b/locale/en-us/intents/list.meal.intent similarity index 100% rename from locale/en-us/list.meal.intent rename to locale/en-us/intents/list.meal.intent diff --git a/locale/en-us/plan.meal.intent b/locale/en-us/intents/plan.meal.intent similarity index 100% rename from locale/en-us/plan.meal.intent rename to locale/en-us/intents/plan.meal.intent diff --git a/locale/en-us/remove.meal.intent b/locale/en-us/intents/remove.meal.intent similarity index 100% rename from locale/en-us/remove.meal.intent rename to locale/en-us/intents/remove.meal.intent diff --git a/package.json b/package.json new file mode 100644 index 0000000..27628c2 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "devDependencies": { + "@mikejgray/ovos-skill-projen": "^0.0.17", + "projen": "^0.79.7" + } +} diff --git a/requirements/requirements-dev.txt b/requirements/requirements-dev.txt new file mode 100644 index 0000000..a109b7f --- /dev/null +++ b/requirements/requirements-dev.txt @@ -0,0 +1,2 @@ +ovos-plugin-manager +pytest diff --git a/requirements/requirements.txt b/requirements/requirements.txt new file mode 100644 index 0000000..85d2a73 --- /dev/null +++ b/requirements/requirements.txt @@ -0,0 +1,2 @@ +ovos-utils~=0.0, >=0.0.30 +ovos-workshop~=0.0, >=0.0.12 diff --git a/settingsmeta.yaml b/settingsmeta.yaml index 5dc8eda..c1ef6fd 100644 --- a/settingsmeta.yaml +++ b/settingsmeta.yaml @@ -1,9 +1,9 @@ skillMetadata: sections: - - name: Options << Name of section + - name: Meal Plan Settings fields: - - name: placeholder + - name: meals type: text - label: Placeholder - value: "" - placeholder: This space for rent + label: Meals + value: "Spaghetti and meatballs,Toasted sandwiches and tomato soup,Chicken noodle soup,Peanut butter and jelly sandwiches" + placeholder: Comma-separated list of meals diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..b9607a8 --- /dev/null +++ b/setup.py @@ -0,0 +1,82 @@ +#!/usr/bin/env python3 +from setuptools import setup +from os import walk, path + +BASEDIR = path.abspath(path.dirname(__file__)) +URL = "https://github.com/mikejgray/skill-meal-plan" +SKILL_CLAZZ = "MealPlanSkill" # needs to match __init__.py class name +PYPI_NAME = "meal-plan-skill" # pip install PYPI_NAME + +# below derived from github url to ensure standard skill_id +SKILL_AUTHOR, SKILL_NAME = URL.split(".com/")[-1].split("/") +SKILL_PKG = SKILL_NAME.lower().replace("-", "_") +PLUGIN_ENTRY_POINT = f"{SKILL_NAME.lower()}.{SKILL_AUTHOR.lower()}={SKILL_PKG}:{SKILL_CLAZZ}" +# skill_id=package_name:SkillClass +BASE_PATH = BASE_PATH = path.abspath(path.join(path.dirname(__file__), ".")) + + +def get_version(): + """Find the version of the package""" + version = None + version_file = path.join(BASE_PATH, "version.py") + major, minor, build, alpha = (None, None, None, None) + with open(version_file) as f: + for line in f: + if "VERSION_MAJOR" in line: + major = line.split("=")[1].strip() + elif "VERSION_MINOR" in line: + minor = line.split("=")[1].strip() + elif "VERSION_BUILD" in line: + build = line.split("=")[1].strip() + elif "VERSION_ALPHA" in line: + alpha = line.split("=")[1].strip() + + if (major and minor and build and alpha) or "# END_VERSION_BLOCK" in line: + break + version = f"{major}.{minor}.{build}" + if alpha and int(alpha) > 0: + version += f"a{alpha}" + return version + + +def get_requirements(requirements_filename: str): + requirements_file = path.join(path.dirname(__file__), requirements_filename) + with open(requirements_file, "r", encoding="utf-8") as r: + requirements = r.readlines() + requirements = [r.strip() for r in requirements if r.strip() and not r.strip().startswith("#")] + return requirements + + +def find_resource_files(): + resource_base_dirs = ("locale", "intents", "dialog", "vocab", "regex", "ui") + package_data = ["*.json"] + for res in resource_base_dirs: + if path.isdir(path.join(BASE_PATH, res)): + for directory, _, files in walk(path.join(BASE_PATH, res)): + if files: + package_data.append(path.join(directory.replace(BASE_PATH, "").lstrip("/"), "*")) + return package_data + + +with open("README.md", "r") as f: + long_description = f.read() + +setup( + name=PYPI_NAME, + version=get_version(), + description="", + long_description=long_description, + long_description_content_type="text/markdown", + url=URL, + author="Mike Gray", + author_email="mike@graywind.org", + license="Apache-2.0", + package_dir={SKILL_PKG: "."}, + package_data={SKILL_PKG: find_resource_files()}, + packages=[SKILL_PKG], + include_package_data=True, + install_requires=get_requirements("requirements/requirements.txt"), + keywords="ovos skill voice assistant", + entry_points={"ovos.plugin.skill": PLUGIN_ENTRY_POINT}, + extras_require={"test": get_requirements("requirements/requirements-dev.txt")}, +) diff --git a/skill.json b/skill.json new file mode 100644 index 0000000..eb7d966 --- /dev/null +++ b/skill.json @@ -0,0 +1,56 @@ +{ + "title": "Meal Plan", + "url": "https://github.com/mikejgray/skill-meal-plan", + "summary": "Suggests a meal for you to make", + "short_description": "Suggests a meal for you to make", + "description": "Suggests a meal for you to make, based on a short default list, but you can manage your own meals!", + "examples": [ + "What should I make for dinner?", + "What should I eat?", + "What should I eat tonight?", + "I'm hungry", + "What's for dinner?" + ], + "desktopFile": false, + "warning": "", + "systemDeps": false, + "requirements": { + "python": [ + "ovos-utils~=0.0, >=0.0.30", + "ovos-workshop~=0.0, >=0.0.12" + ], + "system": {}, + "skill": [] + }, + "incompatible_skills": [], + "platforms": [ + "i386", + "x86_64", + "ia64", + "arm64", + "arm" + ], + "branch": "master", + "license": "Unknown", + "icon": "https://raw.githack.com/FortAwesome/Font-Awesome/master/svgs/solid/utensils.svg", + "category": "Daily", + "categories": [ + "Daily", + "Productivity" + ], + "tags": [ + "Food meal planning", + "Food", + "Meal", + "Planning", + "Meal planning" + ], + "credits": [ + "Mike", + "Gray", + "" + ], + "skillname": "skill-meal-plan", + "authorname": "mikejgray", + "foldername": null +} \ No newline at end of file diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_intents.yaml b/test/test_intents.yaml new file mode 100644 index 0000000..2ef26d0 --- /dev/null +++ b/test/test_intents.yaml @@ -0,0 +1,7 @@ +intents: + # Padatious intents are the `.intent` file names + padatious: + - add.meal.intent + - list.meal.intent + - plan.meal.intent + - remove.meal.intent diff --git a/test/test_main.py b/test/test_main.py new file mode 100644 index 0000000..a72706f --- /dev/null +++ b/test/test_main.py @@ -0,0 +1,12 @@ +# pylint: disable=missing-docstring +import pytest +from ovos_plugin_manager.skills import find_skill_plugins + + +def test_skill_is_a_valid_plugin(): + skills = ",".join(find_skill_plugins().keys()) + assert "skill-meal-plan.mikejgray" in skills + + +if __name__ == "__main__": + pytest.main() diff --git a/test/test_resources.yaml b/test/test_resources.yaml new file mode 100644 index 0000000..014b741 --- /dev/null +++ b/test/test_resources.yaml @@ -0,0 +1,32 @@ +# Specify resources to test here. + +# Specify languages to be tested +languages: + - "en-us" + +# vocab is lowercase .voc file basenames +vocab: [] + +dialog: + - add.meal + - confirm.list.meals + - confirm.remove.meal + - failed.to.add.meal + - failed.to.remove.meal + - list.meals + - meal.added + - meal.removed + - plan.meal + - remove.meal + - skip.list.meals +# regex entities, not necessarily filenames +regex: [] +intents: + # Padatious intents are the `.intent` file names + padatious: + - add.meal.intent + - list.meal.intent + - plan.meal.intent + - remove.meal.intent + # Adapt intents are the name passed to the constructor + adapt: [] diff --git a/version.py b/version.py new file mode 100644 index 0000000..8a2728d --- /dev/null +++ b/version.py @@ -0,0 +1,4 @@ +VERSION_MAJOR = 1 +VERSION_MINOR = 0 +VERSION_BUILD = 0 +VERSION_ALPHA = 0 \ No newline at end of file