api/prepare: do type validation of the input data

This will avoid further pain when we get back the data
to the web ui for instance (rework_ui).
3 files changed, 55 insertions(+), 10 deletions(-)

M rework/io.py
M setup.py
M tests/test_api.py
M rework/io.py +33 -6
@@ 4,7 4,8 @@ from datetime import datetime as dt
 
 from dateutil.parser import (
     isoparse,
-    parse as defaultparse
+    parse as defaultparse,
+    ParserError
 )
 from dateutil.relativedelta import relativedelta
 from psyl import lisp

          
@@ 50,6 51,10 @@ class number(_iobase):
     def binary_encode(self, args):
         val = self.val(args)
         if val is not None:
+            if not isinstance(val, (int, float)):
+                raise TypeError(
+                    f'value `{repr(val)}` is not a number'
+                )
             return str(val).encode('utf-8')
 
     def binary_decode(self, args):

          
@@ 67,6 72,10 @@ class string(_iobase):
     def binary_encode(self, args):
         val = self.val(args)
         if val is not None:
+            if not isinstance(val, str):
+                raise TypeError(
+                    f'value `{repr(val)}` is not a string'
+                )
             return val.encode('utf-8')
 
     def binary_decode(self, args):

          
@@ 78,7 87,13 @@ class string(_iobase):
 class file(_iobase):
 
     def binary_encode(self, args):
-        return self.val(args)
+        val = self.val(args)
+        if val is not None:
+            if not isinstance(val, bytes):
+                raise TypeError(
+                    f'value `{repr(val)}` is not bytes'
+                )
+        return val
 
     def binary_decode(self, args):
         return args.get(self.name)

          
@@ 90,10 105,20 @@ class datetime(_iobase):
         val = self.val(args)
         if val is None:
             return
+        if not isinstance(val, (str, dt)):
+            raise TypeError(
+                f'value `{repr(val)}` is not str/datetime'
+            )
+
         if isinstance(val, str):
-            val = val.encode('utf-8')
-        else:
-            val = val.isoformat().encode('utf-8')
+            try:
+                val = defaultparse(val)
+            except ParserError:
+                raise TypeError(
+                    f'value `{repr(val)}` is not a valid datetime'
+                )
+
+        val = val.isoformat().encode('utf-8')
         return val
 
     def binary_decode(self, args):

          
@@ 140,7 165,9 @@ class moment(_iobase):
             lisp.evaluate(val, env=_MOMENT_ENV)
         except:
             import traceback as tb; tb.print_exc()
-            raise
+            raise TypeError(
+                f'value `{repr(val)}` is not a valid moment expression'
+            )
 
         return val.encode('utf-8')
 

          
M setup.py +1 -1
@@ 26,7 26,7 @@ setup(name='rework',
           'inireader',
           'apscheduler',
           'pyzstd',
-          'dateutils',
+          'python-dateutil',
           'psyl'
       ],
       package_data={'rework': [

          
M tests/test_api.py +21 -3
@@ 266,9 266,27 @@ def test_prepare_with_inputs(engine, cle
     res = engine.execute('select count(*) from rework.sched').scalar()
     assert res == 1
 
+    for name, badvalue in (
+            ('name', 42),
+            ('myfile.txt', 'hello'),
+            ('weight', '65'),
+            ('birthdate', 'lol'),
+    ):
+        failargs = args.copy()
+        failargs[name] = badvalue
+        with pytest.raises(TypeError):
+            api.prepare(
+                engine,
+                'yummy',
+                rule='* * * * * *',
+                _anyrule=True,
+                inputdata=failargs,
+                metadata={'user': 'Babar'}
+            )
+
     failargs = args.copy()
-    failargs['name'] = 42
-    with pytest.raises(AttributeError):
+    failargs['option'] = 3.14
+    with pytest.raises(ValueError):
         api.prepare(
             engine,
             'yummy',

          
@@ 350,7 368,7 @@ def test_prepare_inputs_nr_domain_mismat
     register_tasks()
     api.freeze_operations(engine)
     data = {
-        'history': '0'
+        'history': 0
     }
     with pytest.raises(Exception):
         sid = api.prepare(