From ad1a48ccfc2b63a05c68077474aa0ce7c86c468b Mon Sep 17 00:00:00 2001 From: MarcTM01 <108792843+MarcTM01@users.noreply.github.com> Date: Fri, 17 May 2024 19:01:03 +0200 Subject: [PATCH] Add python-version of the dynamodb model. Implement test-case to verify that everything is parsed properly without validation issues. --- alexa-skill/lambda-pdm/pdm.lock | 82 ++++++++++- alexa-skill/lambda-pdm/pyproject.toml | 5 +- alexa-skill/lambda-pdm/pytest.ini | 2 + alexa-skill/lambda-pdm/src/config.py | 2 + alexa-skill/lambda-pdm/src/data/model.py | 56 +++++++ alexa-skill/lambda-pdm/tests/__init__.py | 0 alexa-skill/lambda-pdm/tests/data/__init__.py | 0 .../lambda-pdm/tests/data/example_model.json | 139 ++++++++++++++++++ .../tests/data/test_model_parsing.py | 16 ++ 9 files changed, 298 insertions(+), 4 deletions(-) create mode 100644 alexa-skill/lambda-pdm/pytest.ini create mode 100644 alexa-skill/lambda-pdm/src/data/model.py create mode 100644 alexa-skill/lambda-pdm/tests/__init__.py create mode 100644 alexa-skill/lambda-pdm/tests/data/__init__.py create mode 100644 alexa-skill/lambda-pdm/tests/data/example_model.json create mode 100644 alexa-skill/lambda-pdm/tests/data/test_model_parsing.py diff --git a/alexa-skill/lambda-pdm/pdm.lock b/alexa-skill/lambda-pdm/pdm.lock index 8e04bc3..2b67556 100644 --- a/alexa-skill/lambda-pdm/pdm.lock +++ b/alexa-skill/lambda-pdm/pdm.lock @@ -2,10 +2,10 @@ # It is not intended for manual editing. [metadata] -groups = ["default", "lint", "typecheck", "i18n"] +groups = ["default", "lint", "typecheck", "i18n", "test"] strategy = ["cross_platform", "inherit_metadata"] lock_version = "4.4.1" -content_hash = "sha256:b66db6a2156840264ec5bc6cbc30e4133a75f8f35e935505a91712adec035bb4" +content_hash = "sha256:8f1390956d936c94b9eb8e0f94fbb18fb8480d263002d5b797e9a59954d7b9b0" [[package]] name = "annotated-types" @@ -199,6 +199,30 @@ files = [ {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, ] +[[package]] +name = "colorama" +version = "0.4.6" +requires_python = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +summary = "Cross-platform colored terminal text." +groups = ["test"] +marker = "sys_platform == \"win32\"" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.1" +requires_python = ">=3.7" +summary = "Backport of PEP 654 (exception groups)" +groups = ["test"] +marker = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.1-py3-none-any.whl", hash = "sha256:5258b9ed329c5bbdd31a309f53cbfb0b155341807f6ff7606a1e801a891b29ad"}, + {file = "exceptiongroup-1.2.1.tar.gz", hash = "sha256:a4785e48b045528f5bfe627b6ad554ff32def154f42372786903b7abcfe1aa16"}, +] + [[package]] name = "idna" version = "3.7" @@ -210,6 +234,17 @@ files = [ {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, ] +[[package]] +name = "iniconfig" +version = "2.0.0" +requires_python = ">=3.7" +summary = "brain-dead simple config-ini parsing" +groups = ["test"] +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "jmespath" version = "1.0.1" @@ -351,6 +386,28 @@ files = [ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, ] +[[package]] +name = "packaging" +version = "24.0" +requires_python = ">=3.7" +summary = "Core utilities for Python packages" +groups = ["test"] +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +requires_python = ">=3.8" +summary = "plugin and hook calling mechanisms for python" +groups = ["test"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + [[package]] name = "pydantic" version = "2.7.1" @@ -423,6 +480,25 @@ files = [ {file = "pydantic_settings-2.2.1.tar.gz", hash = "sha256:00b9f6a5e95553590434c0fa01ead0b216c3e10bc54ae02e37f359948643c5ed"}, ] +[[package]] +name = "pytest" +version = "8.2.0" +requires_python = ">=3.8" +summary = "pytest: simple powerful testing with Python" +groups = ["test"] +dependencies = [ + "colorama; sys_platform == \"win32\"", + "exceptiongroup>=1.0.0rc8; python_version < \"3.11\"", + "iniconfig", + "packaging", + "pluggy<2.0,>=1.5", + "tomli>=1; python_version < \"3.11\"", +] +files = [ + {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, + {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -532,7 +608,7 @@ name = "tomli" version = "2.0.1" requires_python = ">=3.7" summary = "A lil' TOML parser" -groups = ["typecheck"] +groups = ["test", "typecheck"] marker = "python_version < \"3.11\"" files = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, diff --git a/alexa-skill/lambda-pdm/pyproject.toml b/alexa-skill/lambda-pdm/pyproject.toml index bc881a4..a36a8b1 100644 --- a/alexa-skill/lambda-pdm/pyproject.toml +++ b/alexa-skill/lambda-pdm/pyproject.toml @@ -19,6 +19,7 @@ license = {text = "MIT"} lint = "ruff check --fix ." format = "ruff format ." typecheck = "mypy ./src/" +test = "pytest tests/" [tool.pdm.dev-dependencies] lint = [ @@ -31,6 +32,9 @@ typecheck = [ i18n = [ "babel>=2.15.0", ] +test = [ + "pytest>=8.2.0", +] [tool.ruff.lint] select = [ @@ -52,4 +56,3 @@ convention = "pep257" [tool.pdm] distribution = false - diff --git a/alexa-skill/lambda-pdm/pytest.ini b/alexa-skill/lambda-pdm/pytest.ini new file mode 100644 index 0000000..ce20569 --- /dev/null +++ b/alexa-skill/lambda-pdm/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +pythonpath = "src" diff --git a/alexa-skill/lambda-pdm/src/config.py b/alexa-skill/lambda-pdm/src/config.py index e15f4d7..e98507d 100644 --- a/alexa-skill/lambda-pdm/src/config.py +++ b/alexa-skill/lambda-pdm/src/config.py @@ -4,6 +4,7 @@ class DynamoDBConfig(BaseModel): """A configuration class for the DynamoDB table connection.""" + table_name: str region: str assume_role_arn: str @@ -12,6 +13,7 @@ class DynamoDBConfig(BaseModel): class Config(BaseSettings): """The main configuration class for the skill.""" + model_config = SettingsConfigDict(env_nested_delimiter='__', env_file='.env', env_file_encoding='utf-8') dynamodb_config: DynamoDBConfig diff --git a/alexa-skill/lambda-pdm/src/data/model.py b/alexa-skill/lambda-pdm/src/data/model.py new file mode 100644 index 0000000..a5c6c6b --- /dev/null +++ b/alexa-skill/lambda-pdm/src/data/model.py @@ -0,0 +1,56 @@ +import datetime +from enum import Enum +from typing import List, Optional, Set + +from pydantic import BaseModel, Field, field_validator + + +class NutritionFlag(str, Enum): + """An enumeration of the possible nutrition flags.""" + + VEGETARIAN = "vegetarian" + VEGAN = "vegan" + PORK = "pork" + BEEF = "beef" + FISH = "fish" + CHICKEN = "chicken" + + +class MensaMenuExtra(BaseModel): + """A model for the extra menu items.""" + + name: str = Field(alias='Name') + description: str = Field(alias='Description') + + +class MensaMenu(BaseModel): + """A model for the menu items.""" + + name: str = Field(alias='Name') + contents: List[str] = Field(alias='Contents') + price: Optional[str] = Field(alias='Price', default=None) + nutrition_flags: Set[NutritionFlag] = Field(alias='NutritionFlags', default_factory=set) + + @field_validator('nutrition_flags', mode='before') + def parse_nutrition_flags(cls, value): + """Parse the nutrition flags from the expected input format to a set of NutritionFlag enums. + + Sample input value: { + "NutritionFlags": { + "vegetarian": {} + }, + } + """ + if isinstance(value, dict): + return {NutritionFlag(flag) for flag in value.keys()} + return value + + +class MensaDayMenus(BaseModel): + """A model for the dynamodb mensa menu item.""" + + date: datetime.date = Field(alias='Date') + menus: List[MensaMenu] = Field(alias='Menus') + extras: List[MensaMenuExtra] = Field(alias='Extras') + mensa_id: str = Field(alias='MensaId') + LanguageKey: str = Field(alias='LanguageKey') diff --git a/alexa-skill/lambda-pdm/tests/__init__.py b/alexa-skill/lambda-pdm/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alexa-skill/lambda-pdm/tests/data/__init__.py b/alexa-skill/lambda-pdm/tests/data/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/alexa-skill/lambda-pdm/tests/data/example_model.json b/alexa-skill/lambda-pdm/tests/data/example_model.json new file mode 100644 index 0000000..f1ec3d5 --- /dev/null +++ b/alexa-skill/lambda-pdm/tests/data/example_model.json @@ -0,0 +1,139 @@ +{ + "MensaId": "mensa-academica", + "Date": "2024-05-15", + "Menus": [ + { + "NutritionFlags": { + "fish": {} + }, + "Contents": [ + "Potato creme soup", + "Smoked salmon", + "Bread roll" + ], + "Price": "2,00 €", + "Name": "Stew" + }, + { + "NutritionFlags": { + "vegetarian": {} + }, + "Contents": [ + "Potato creme soup", + "Bread roll" + ], + "Price": "1,60 €", + "Name": "Vegetarian table dish" + }, + { + "NutritionFlags": { + "vegetarian": {}, + "vegan": {} + }, + "Contents": [ + "Crispy sesame-carrot-stick", + "Sweet sour sauce" + ], + "Price": "2,20 €", + "Name": "Vegetarian" + }, + { + "NutritionFlags": { + "pork": {} + }, + "Contents": [ + "Asparagus with ham", + "White wine hollandaise", + "Potatoes" + ], + "Price": "6,50 €", + "Name": "Suggestion of the day" + }, + { + "NutritionFlags": { + "beef": {} + }, + "Contents": [ + "Beef strips", + "Mushrooms" + ], + "Price": "2,80 €", + "Name": "Classics" + }, + { + "NutritionFlags": { + "chicken": {} + }, + "Contents": [ + "Sweet n spicy chicken", + "Banana peanut sauce", + "Basmati rice" + ], + "Price": "3,80 €", + "Name": "Wok" + }, + { + "NutritionFlags": { + "vegetarian": {}, + "vegan": {} + }, + "Contents": [ + "Sweet-spicy vegetables", + "Banana peanut sauce", + "Basmati rice" + ], + "Price": "3,80 €", + "Name": "Wok" + }, + { + "NutritionFlags": { + "beef": {} + }, + "Contents": [ + "Cheeseburger", + "French fries", + "Soft drink 0,33 L" + ], + "Price": "4,90 €", + "Name": "Burger Classics" + }, + { + "NutritionFlags": { + "vegetarian": {} + }, + "Contents": [ + "Veggie burger", + "French fries", + "Soft drink 0,33 L" + ], + "Price": "4,90 €", + "Name": "Burger Classics" + }, + { + "NutritionFlags": { + "beef": {} + }, + "Contents": [ + "Italian burger", + "Rocket, tomatoes", + "cheese sauce", + "French fries", + "Soft drink 0,33 L" + ], + "Price": "4,90 €", + "Name": "Burger of the week" + } + ], + "Extras": [ + { + "Name": "Main side-dish", + "Description": "potatoes or rice" + }, + { + "Name": "secondary", + "Description": "Romanesco or Mixed salad" + } + ], + "LanguageKey": "en", + "MensaIdLanguageKeyDate": "mensa-academica;en;2024-05-15" +} diff --git a/alexa-skill/lambda-pdm/tests/data/test_model_parsing.py b/alexa-skill/lambda-pdm/tests/data/test_model_parsing.py new file mode 100644 index 0000000..75e17b1 --- /dev/null +++ b/alexa-skill/lambda-pdm/tests/data/test_model_parsing.py @@ -0,0 +1,16 @@ +import json +from pathlib import Path + +from data.model import MensaDayMenus + + +def test_model_parsing(): + """Test the parsing of the model from a json file.""" + model_path = Path(__file__).parent / 'example_model.json' + assert model_path.exists() + assert model_path.is_file() + + with open(model_path, 'r') as f: + model = json.load(f) + + MensaDayMenus(**model)