diff --git a/setup.py b/setup.py index 268cdf48..14fba73b 100644 --- a/setup.py +++ b/setup.py @@ -16,7 +16,7 @@ long_description_content_type="text/markdown", url="https://simvue.io", platforms=["any"], - install_requires=["requests", "msgpack", "tenacity", "pyjwt", "psutil"], + install_requires=["requests", "msgpack", "tenacity", "pyjwt", "psutil", "pydantic"], package_dir={'': '.'}, packages=["simvue"], package_data={"": ["README.md"]}, diff --git a/simvue/__init__.py b/simvue/__init__.py index 745d6106..6b9c85da 100644 --- a/simvue/__init__.py +++ b/simvue/__init__.py @@ -1,4 +1,5 @@ from simvue.run import Run from simvue.client import Client from simvue.handler import Handler +from simvue.models import RunInput __version__ = '0.7.1' diff --git a/simvue/models.py b/simvue/models.py new file mode 100644 index 00000000..cf282193 --- /dev/null +++ b/simvue/models.py @@ -0,0 +1,17 @@ +from datetime import datetime + +import re +from pydantic import BaseModel, constr +from typing import Optional, List, Dict, Union +from enum import Enum + +FolderStrRegex = constr(regex=r"^/.*") + +# Pydantic class to validate run.init() +class RunInput(BaseModel): + name: Optional[str] + metadata: Optional[Dict[str, Union[str, int, float, None]]] + tags: Optional[List[str]] + description: Optional[str] + folder: FolderStrRegex + status: Optional[str] diff --git a/simvue/run.py b/simvue/run.py index 5dd36674..51af4587 100644 --- a/simvue/run.py +++ b/simvue/run.py @@ -16,7 +16,9 @@ from .worker import Worker from .simvue import Simvue +from .models import RunInput from .utilities import get_auth, get_expiry +from pydantic import ValidationError INIT_MISSING = 'initialize a run using init() first' QUEUE_SIZE = 10000 @@ -226,12 +228,6 @@ def init(self, name=None, metadata={}, tags=[], description=None, folder='/', ru if not re.match(r'^[a-zA-Z0-9\-\_\s\/\.:]+$', name): self._error('specified name is invalid') - if not isinstance(tags, list): - self._error('tags must be a list') - - if not isinstance(metadata, dict): - self._error('metadata must be a dict') - self._name = name if running: @@ -252,9 +248,6 @@ def init(self, name=None, metadata={}, tags=[], description=None, folder='/', ru if description: data['description'] = description - if not folder.startswith('/'): - self._error('the folder must begin with /') - data['folder'] = folder if self._status == 'running': @@ -262,6 +255,12 @@ def init(self, name=None, metadata={}, tags=[], description=None, folder='/', ru self._check_token() + # compare with pydantic RunInput model + try: + runinput = RunInput(**data) + except ValidationError as e: + self._error(e) + self._simvue = Simvue(self._name, self._uuid, self._mode, self._suppress_errors) name = self._simvue.create_run(data) diff --git a/tests/unit/test_simvue.py b/tests/unit/test_simvue.py index dde5abe5..b507b79b 100644 --- a/tests/unit/test_simvue.py +++ b/tests/unit/test_simvue.py @@ -10,3 +10,61 @@ def test_suppress_errors(): with pytest.raises(RuntimeError, match="suppress_errors must be boolean"): run.config(suppress_errors=200) + +def test_run_init_metadata(): + """ + Check that run.init throws an exception if tuples are passed into metadata dictionary + """ + os.environ["SIMVUE_TOKEN"] = "test" + os.environ["SIMVUE_URL"] = "https://simvue.io" + + x1_lower = 2, + x1_upper = 6, + + run = Run(mode='offline') + + with pytest.raises(RuntimeError) as exc_info: + run.init(metadata={'dataset.x1_lower': x1_lower, 'dataset.x1_upper': x1_upper}, + description="A test to validate inputs passed into metadata dictionary" + ) + + assert exc_info.match(r"value is not a valid integer") + +def test_run_init_tags(): + """ + Check that run.init throws an exception if tags are not a list + """ + os.environ["SIMVUE_TOKEN"] = "test" + os.environ["SIMVUE_URL"] = "https://simvue.io" + + x1_lower = 2 + x1_upper = 6 + + run = Run(mode='offline') + + with pytest.raises(RuntimeError) as exc_info: + run.init(metadata={'dataset.x1_lower': x1_lower, 'dataset.x1_upper': x1_upper}, tags=1, + description="A test to validate tag inputs passed into run.init" + ) + + assert exc_info.match(r"value is not a valid list") + +def test_run_init_folder(): + """ + Check that run.init throws an exception if folder input is not specified correctly + """ + os.environ["SIMVUE_TOKEN"] = "test" + os.environ["SIMVUE_URL"] = "https://simvue.io" + + x1_lower = 2 + x1_upper = 6 + + run = Run(mode='offline') + + with pytest.raises(RuntimeError) as exc_info: + run.init(metadata={'dataset.x1_lower': x1_lower, 'dataset.x1_upper': x1_upper}, tags=[1,2,3], folder='test_folder', + description="A test to validate folder input passed into run.init" + ) + + assert exc_info.match(r"string does not match regex") +