clams-python 1.1.3__tar.gz → 1.1.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {clams-python-1.1.3/clams_python.egg-info → clams-python-1.1.5}/PKG-INFO +2 -2
- clams-python-1.1.5/VERSION +1 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/app/__init__.py +150 -30
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/appmetadata/__init__.py +83 -23
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/restify/__init__.py +6 -88
- clams-python-1.1.5/clams/ver/__init__.py +1 -0
- {clams-python-1.1.3 → clams-python-1.1.5/clams_python.egg-info}/PKG-INFO +2 -2
- {clams-python-1.1.3 → clams-python-1.1.5}/clams_python.egg-info/requires.txt +1 -1
- {clams-python-1.1.3 → clams-python-1.1.5}/requirements.txt +1 -1
- {clams-python-1.1.3 → clams-python-1.1.5}/tests/test_clamsapp.py +85 -31
- clams-python-1.1.3/VERSION +0 -1
- clams-python-1.1.3/clams/ver/__init__.py +0 -1
- {clams-python-1.1.3 → clams-python-1.1.5}/LICENSE +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/MANIFEST.in +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/README.md +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/__init__.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/__init__.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/.dockerignore.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/.gitignore.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/Containerfile.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/LICENSE.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/README.md.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/app.py.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/metadata.py.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/requirements.txt.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/mmif_utils/__init__.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/mmif_utils/rewind.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/mmif_utils/source.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams/serve/__init__.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams_python.egg-info/SOURCES.txt +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams_python.egg-info/dependency_links.txt +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams_python.egg-info/entry_points.txt +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/clams_python.egg-info/top_level.txt +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/setup.cfg +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/setup.py +0 -0
- {clams-python-1.1.3 → clams-python-1.1.5}/tests/test_clamscli.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clams-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
4
4
|
Summary: A collection of APIs to develop CLAMS app for python
|
|
5
5
|
Home-page: https://clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
14
14
|
Requires-Python: >=3.8
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: mmif-python==1.0.
|
|
17
|
+
Requires-Dist: mmif-python==1.0.11
|
|
18
18
|
Requires-Dist: Flask>=2
|
|
19
19
|
Requires-Dist: Flask-RESTful>=0.3.9
|
|
20
20
|
Requires-Dist: gunicorn>=20
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
1.1.5
|
|
@@ -9,10 +9,10 @@ from urllib import parse as urlparser
|
|
|
9
9
|
|
|
10
10
|
__all__ = ['ClamsApp']
|
|
11
11
|
|
|
12
|
-
from typing import Union, Any, Optional
|
|
12
|
+
from typing import Union, Any, Optional, Dict, List, Tuple
|
|
13
13
|
|
|
14
14
|
from mmif import Mmif, Document, DocumentTypes, View
|
|
15
|
-
from clams.appmetadata import AppMetadata
|
|
15
|
+
from clams.appmetadata import AppMetadata, real_valued_primitives, python_type, map_param_kv_delimiter
|
|
16
16
|
|
|
17
17
|
logging.basicConfig(
|
|
18
18
|
level=logging.WARNING,
|
|
@@ -28,12 +28,14 @@ class ClamsApp(ABC):
|
|
|
28
28
|
"""
|
|
29
29
|
# A set of "universal runtime parameters that can be used for both GET and POST anytime".
|
|
30
30
|
# The behavioral changes based on these parameters must be implemented on the SDK level.
|
|
31
|
+
# This needs to stay as a list of dicts for compatibility with the metadata.py file (templated).
|
|
31
32
|
universal_parameters = [
|
|
32
33
|
{
|
|
33
34
|
'name': 'pretty', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False,
|
|
34
35
|
'description': 'The JSON body of the HTTP response will be re-formatted with 2-space indentation',
|
|
35
36
|
},
|
|
36
37
|
]
|
|
38
|
+
|
|
37
39
|
# this key is used to store users' raw input params in the parameter dict
|
|
38
40
|
# even after "refinement" (i.e., casting to proper data types)
|
|
39
41
|
_RAW_PARAMS_KEY = "#RAW#"
|
|
@@ -41,25 +43,30 @@ class ClamsApp(ABC):
|
|
|
41
43
|
def __init__(self):
|
|
42
44
|
self.metadata: AppMetadata = self._load_appmetadata()
|
|
43
45
|
super().__init__()
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
|
|
47
|
+
# data type spec to be used in type caster
|
|
47
48
|
self.metadata_param_spec = {}
|
|
48
49
|
self.annotate_param_spec = {}
|
|
49
50
|
for param in ClamsApp.universal_parameters:
|
|
50
|
-
|
|
51
|
-
self.
|
|
51
|
+
# in addition to building the param spec, add them as regular params, so they are serialized in metadata
|
|
52
|
+
self.metadata.add_parameter(**param)
|
|
53
|
+
self.metadata_param_spec[param['name']] = (param['type'], param.get('multivalued', False))
|
|
52
54
|
for param_spec in self.metadata.parameters:
|
|
53
|
-
self.annotate_param_spec[param_spec.name] = (
|
|
55
|
+
self.annotate_param_spec[param_spec.name] = (param_spec.type, param_spec.multivalued)
|
|
56
|
+
|
|
57
|
+
self.metadata_param_caster = ParameterCaster(self.metadata_param_spec)
|
|
58
|
+
self.annotate_param_caster = ParameterCaster(self.annotate_param_spec)
|
|
54
59
|
self.logger = logging.getLogger(self.metadata.identifier)
|
|
55
60
|
|
|
56
|
-
def appmetadata(self, **kwargs) -> str:
|
|
61
|
+
def appmetadata(self, **kwargs: List[str]) -> str:
|
|
57
62
|
"""
|
|
58
63
|
A public method to get metadata for this app as a string.
|
|
59
64
|
|
|
60
65
|
:return: Serialized JSON string of the metadata
|
|
61
66
|
"""
|
|
62
|
-
|
|
67
|
+
# cast only, no refinement
|
|
68
|
+
casted = self.metadata_param_caster.cast(kwargs)
|
|
69
|
+
pretty = casted.pop('pretty') if 'pretty' in casted else False
|
|
63
70
|
return self.metadata.jsonify(pretty)
|
|
64
71
|
|
|
65
72
|
def _load_appmetadata(self) -> AppMetadata:
|
|
@@ -98,7 +105,7 @@ class ClamsApp(ABC):
|
|
|
98
105
|
def _check_mmif_compatibility(target_specver, input_specver):
|
|
99
106
|
return target_specver.split('.')[:2] == input_specver.split('.')[:2]
|
|
100
107
|
|
|
101
|
-
def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params) -> str:
|
|
108
|
+
def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> str:
|
|
102
109
|
"""
|
|
103
110
|
A public method to invoke the primary app function. It's essentially a
|
|
104
111
|
wrapper around :meth:`~clams.app.ClamsApp._annotate` method where some common operations
|
|
@@ -108,21 +115,22 @@ class ClamsApp(ABC):
|
|
|
108
115
|
:param runtime_params: An arbitrary set of k-v pairs to configure the app at runtime
|
|
109
116
|
:return: Serialized JSON string of the output of the app
|
|
110
117
|
"""
|
|
111
|
-
pretty = runtime_params.get('pretty', False)
|
|
112
118
|
if not isinstance(mmif, Mmif):
|
|
113
119
|
mmif = Mmif(mmif)
|
|
114
120
|
issued_warnings = []
|
|
115
121
|
for key in runtime_params:
|
|
116
122
|
if key not in self.annotate_param_spec:
|
|
117
123
|
issued_warnings.append(UserWarning(f'An undefined parameter "{key}" (value: "{runtime_params[key]}") is passed'))
|
|
118
|
-
|
|
124
|
+
# this will do casting + refinement altogether
|
|
125
|
+
refined = self._refine_params(**runtime_params)
|
|
126
|
+
pretty = refined.get('pretty', False)
|
|
119
127
|
with warnings.catch_warnings(record=True) as ws:
|
|
120
|
-
annotated = self._annotate(mmif, **
|
|
128
|
+
annotated = self._annotate(mmif, **refined)
|
|
121
129
|
if ws:
|
|
122
130
|
issued_warnings.extend(ws)
|
|
123
131
|
if issued_warnings:
|
|
124
132
|
warnings_view = annotated.new_view()
|
|
125
|
-
self.sign_view(warnings_view,
|
|
133
|
+
self.sign_view(warnings_view, runtime_params)
|
|
126
134
|
warnings_view.metadata.warnings = issued_warnings
|
|
127
135
|
return annotated.serialize(pretty=pretty, sanitize=True)
|
|
128
136
|
|
|
@@ -148,7 +156,7 @@ class ClamsApp(ABC):
|
|
|
148
156
|
"""
|
|
149
157
|
raise NotImplementedError()
|
|
150
158
|
|
|
151
|
-
def _refine_params(self, **runtime_params):
|
|
159
|
+
def _refine_params(self, **runtime_params: List[str]):
|
|
152
160
|
"""
|
|
153
161
|
Method to "fill" the parameter dictionary with default values, when a key-value is not specified in the input.
|
|
154
162
|
The input map is not really "filled" as a copy of it is returned with addition of default values.
|
|
@@ -159,19 +167,21 @@ class ClamsApp(ABC):
|
|
|
159
167
|
if self._RAW_PARAMS_KEY in runtime_params:
|
|
160
168
|
# meaning the dict is already refined, just return it
|
|
161
169
|
return runtime_params
|
|
162
|
-
|
|
170
|
+
refined = {}
|
|
171
|
+
|
|
172
|
+
casted = self.annotate_param_caster.cast(runtime_params)
|
|
163
173
|
for parameter in self.metadata.parameters:
|
|
164
|
-
if parameter.name in
|
|
165
|
-
if parameter.choices and
|
|
174
|
+
if parameter.name in casted:
|
|
175
|
+
if parameter.choices and casted[parameter.name] not in parameter.choices:
|
|
166
176
|
raise ValueError(f"Value for parameter \"{parameter.name}\" must be one of {parameter.choices}.")
|
|
167
|
-
|
|
177
|
+
refined[parameter.name] = casted[parameter.name]
|
|
168
178
|
elif parameter.default is not None:
|
|
169
|
-
|
|
179
|
+
refined[parameter.name] = parameter.default
|
|
170
180
|
else:
|
|
171
181
|
raise ValueError(f"Cannot find configuration for a required parameter \"{parameter.name}\".")
|
|
172
182
|
# raw input params are hidden under a special key
|
|
173
|
-
|
|
174
|
-
return
|
|
183
|
+
refined[self._RAW_PARAMS_KEY] = runtime_params
|
|
184
|
+
return refined
|
|
175
185
|
|
|
176
186
|
def get_configuration(self, **runtime_params):
|
|
177
187
|
warnings.warn("ClamsApp.get_configuration() is deprecated. "
|
|
@@ -197,15 +207,26 @@ class ClamsApp(ABC):
|
|
|
197
207
|
"no longer be optional in the future. Please pass `runtime_params` "
|
|
198
208
|
"from _annotate() method.",
|
|
199
209
|
FutureWarning, stacklevel=2)
|
|
210
|
+
return
|
|
200
211
|
view.metadata.app = self.metadata.identifier
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
212
|
+
if self._RAW_PARAMS_KEY in runtime_conf:
|
|
213
|
+
for k, v in runtime_conf.items():
|
|
214
|
+
if k == self._RAW_PARAMS_KEY:
|
|
215
|
+
for orik, oriv in v.items():
|
|
216
|
+
if orik in self.metadata.parameters and self.metadata.parameters[orik].multivalued:
|
|
217
|
+
view.metadata.add_parameter(orik, str(oriv))
|
|
218
|
+
else:
|
|
219
|
+
view.metadata.add_parameter(orik, oriv[0])
|
|
220
|
+
view.metadata.add_app_configuration(k, v)
|
|
221
|
+
else:
|
|
222
|
+
# meaning the parameters directly from flask or argparser and values are in lists
|
|
223
|
+
for k, v in runtime_conf.items():
|
|
224
|
+
if k in self.metadata.parameters and self.metadata.parameters[k].multivalued:
|
|
225
|
+
view.metadata.add_parameter(k, str(v))
|
|
226
|
+
else:
|
|
227
|
+
view.metadata.add_parameter(k, v[0])
|
|
207
228
|
|
|
208
|
-
def set_error_view(self, mmif: Union[str, dict, Mmif], runtime_conf:
|
|
229
|
+
def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str]) -> Mmif:
|
|
209
230
|
"""
|
|
210
231
|
A method to record an error instead of annotation results in the view
|
|
211
232
|
this app generated. For logging purpose, the runtime parameters used
|
|
@@ -282,3 +303,102 @@ class ClamsApp(ABC):
|
|
|
282
303
|
document_file.close()
|
|
283
304
|
else:
|
|
284
305
|
raise FileNotFoundError(p.path)
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class ParameterCaster(object):
|
|
309
|
+
|
|
310
|
+
def __init__(self, param_spec: Dict[str, Tuple[str, bool]]):
|
|
311
|
+
"""
|
|
312
|
+
A helper class to convert parameters passed by HTTP query strings to
|
|
313
|
+
proper python data types.
|
|
314
|
+
|
|
315
|
+
:param param_spec: A specification of a data types of parameters
|
|
316
|
+
"""
|
|
317
|
+
self.param_spec = {pname: (python_type[ptype_str], pmultivalued)
|
|
318
|
+
for pname, (ptype_str, pmultivalued) in param_spec.items()}
|
|
319
|
+
|
|
320
|
+
def cast(self, args: Dict[str, List[str]]) \
|
|
321
|
+
-> Dict[str, Union[real_valued_primitives, List[real_valued_primitives], Dict[str, str]]]:
|
|
322
|
+
"""
|
|
323
|
+
Given parameter specification, tries to cast values of args to specified Python data types.
|
|
324
|
+
Note that this caster deals with query strings, thus all keys and values in the input args are plain strings.
|
|
325
|
+
Also note that the caster does not handle "unexpected" parameters came as an input.
|
|
326
|
+
Handling (raising an exception or issuing a warning upon receiving) an unexpected runtime parameter
|
|
327
|
+
must be done within the app itself.
|
|
328
|
+
Thus, when a key is not found in the parameter specifications, it should just pass it as a vanilla string.
|
|
329
|
+
|
|
330
|
+
:param args: k-v pairs
|
|
331
|
+
:return: A new dictionary of type-casted args, of which keys are always strings (parameter name),
|
|
332
|
+
and values are either
|
|
333
|
+
1) a single value of a specified type (multivalued=False)
|
|
334
|
+
2) a list of values of a specified type (multivalued=True) (all duplicates in the input are kept)
|
|
335
|
+
3) a nested string-string dictionary (type=map ⊨ multivalued=True)
|
|
336
|
+
With the third case, developers can further process the nested values into a more complex data types or
|
|
337
|
+
structures, but that is not in the scope of this Caster class.
|
|
338
|
+
"""
|
|
339
|
+
casted = {}
|
|
340
|
+
for k, vs in args.items():
|
|
341
|
+
assert isinstance(vs, list), f"Expected a list of values for key {k}, but got {vs} of type {type(vs)}"
|
|
342
|
+
assert all(isinstance(v, str) for v in vs), f"Expected a list of strings for key {k}, but got {vs} of types {[type(v) for v in vs]}"
|
|
343
|
+
if k in self.param_spec:
|
|
344
|
+
for v in vs:
|
|
345
|
+
valuetype, multivalued = self.param_spec[k]
|
|
346
|
+
if multivalued or k not in casted: # effectively only keeps the first value for non-multi params
|
|
347
|
+
if valuetype == bool:
|
|
348
|
+
v = self.bool_param(v)
|
|
349
|
+
elif valuetype == float:
|
|
350
|
+
v = self.float_param(v)
|
|
351
|
+
elif valuetype == int:
|
|
352
|
+
v = self.int_param(v)
|
|
353
|
+
elif valuetype == str:
|
|
354
|
+
v = self.str_param(v)
|
|
355
|
+
elif valuetype == dict:
|
|
356
|
+
v = self.kv_param(v)
|
|
357
|
+
if multivalued:
|
|
358
|
+
if valuetype == dict:
|
|
359
|
+
casted.setdefault(k, {}).update(v)
|
|
360
|
+
else:
|
|
361
|
+
casted.setdefault(k, []).append(v)
|
|
362
|
+
else:
|
|
363
|
+
casted[k] = v
|
|
364
|
+
else:
|
|
365
|
+
if len(vs) > 1:
|
|
366
|
+
casted[k] = vs
|
|
367
|
+
else:
|
|
368
|
+
casted[k] = vs[0]
|
|
369
|
+
return casted # pytype: disable=bad-return-type
|
|
370
|
+
|
|
371
|
+
@staticmethod
|
|
372
|
+
def bool_param(value) -> bool:
|
|
373
|
+
"""
|
|
374
|
+
Helper function to convert string values to bool type.
|
|
375
|
+
"""
|
|
376
|
+
return False if value in (False, 0, 'False', 'false', '0') else True
|
|
377
|
+
|
|
378
|
+
@staticmethod
|
|
379
|
+
def float_param(value) -> float:
|
|
380
|
+
"""
|
|
381
|
+
Helper function to convert string values to float type.
|
|
382
|
+
"""
|
|
383
|
+
return float(value)
|
|
384
|
+
|
|
385
|
+
@staticmethod
|
|
386
|
+
def int_param(value) -> int:
|
|
387
|
+
"""
|
|
388
|
+
Helper function to convert string values to int type.
|
|
389
|
+
"""
|
|
390
|
+
return int(value)
|
|
391
|
+
|
|
392
|
+
@staticmethod
|
|
393
|
+
def str_param(value) -> str:
|
|
394
|
+
"""
|
|
395
|
+
Helper function to convert string values to string type.
|
|
396
|
+
"""
|
|
397
|
+
return value
|
|
398
|
+
|
|
399
|
+
@staticmethod
|
|
400
|
+
def kv_param(value) -> Dict[str, str]:
|
|
401
|
+
"""
|
|
402
|
+
Helper function to convert string values to key-value pair type.
|
|
403
|
+
"""
|
|
404
|
+
return dict([value.split(map_param_kv_delimiter, 1)])
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import shutil
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
|
-
import shutil
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from typing import Union, Dict, List, Optional, Literal
|
|
7
7
|
|
|
@@ -14,7 +14,12 @@ app_version_envvar_key = 'CLAMS_APP_VERSION'
|
|
|
14
14
|
# type aliases to use in app metadata and runtime parameter processing
|
|
15
15
|
real_valued_primitives = Union[int, float, bool, str]
|
|
16
16
|
# these names are taken from the JSON schema data types
|
|
17
|
-
param_value_types = Literal['integer', 'number', 'string', 'boolean']
|
|
17
|
+
param_value_types = Literal['integer', 'number', 'string', 'boolean', 'map']
|
|
18
|
+
# and the specific delimiter for the map type
|
|
19
|
+
map_param_kv_delimiter = ':'
|
|
20
|
+
|
|
21
|
+
# dict to map app parameter data types to Python data types
|
|
22
|
+
python_type = {"boolean": bool, "number": float, "integer": int, "string": str, "map": dict}
|
|
18
23
|
|
|
19
24
|
param_value_types_values = param_value_types.__args__ # pytype: disable=attribute-error
|
|
20
25
|
app_directory_baseurl = "http://apps.clams.ai"
|
|
@@ -38,9 +43,12 @@ def generate_app_version(cwd=None):
|
|
|
38
43
|
gitcmd = shutil.which('git')
|
|
39
44
|
gitdir = (Path(sys.modules["__main__"].__file__).parent.resolve() if cwd is None else Path(cwd)) / '.git'
|
|
40
45
|
if gitcmd is not None and gitdir.exists():
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
46
|
+
try:
|
|
47
|
+
proc = subprocess.run([gitcmd, '--git-dir', str(gitdir), 'describe', '--tags', '--always'],
|
|
48
|
+
capture_output=True, check=True)
|
|
49
|
+
return proc.stdout.decode('utf8').strip()
|
|
50
|
+
except subprocess.CalledProcessError:
|
|
51
|
+
return unresolved_app_version_num
|
|
44
52
|
elif app_version_envvar_key in os.environ:
|
|
45
53
|
return os.environ[app_version_envvar_key]
|
|
46
54
|
else:
|
|
@@ -65,20 +73,29 @@ class Output(_BaseModel):
|
|
|
65
73
|
Data model that describes output specification of a CLAMS app.
|
|
66
74
|
|
|
67
75
|
CLAMS apps are expected to have at least one output type, and each type must
|
|
68
|
-
be defined by a ``@type`` URI string. If the type has
|
|
76
|
+
be defined by a ``@type`` URI string. If the type has common properties and values generated by the app,
|
|
69
77
|
they can be described in the (optional) ``properties`` field. Finally, a human-readable
|
|
70
78
|
verbose description can be provided in the (optional) ``description`` field for users.
|
|
71
79
|
|
|
72
|
-
Developers should take diligent care to include all output types and their properties in the app metadata.
|
|
80
|
+
Developers should take diligent care to include all output types and their properties in the app metadata. To
|
|
81
|
+
specify the property values, developers can use an actual value (for full match) or ``'*'`` (for any value).
|
|
73
82
|
"""
|
|
74
83
|
at_type: pydantic.AnyHttpUrl = pydantic.Field(
|
|
75
84
|
...,
|
|
76
85
|
alias="@type",
|
|
77
86
|
description="The type of the object. Must be a IRI string."
|
|
78
87
|
)
|
|
88
|
+
description: Optional[str] = pydantic.Field(
|
|
89
|
+
None,
|
|
90
|
+
description="A verbose, human-readable description of the type. This is intended to be used for documentation "
|
|
91
|
+
"purpose for a particular use case of this annotation type and is not expected to be consumed by "
|
|
92
|
+
"software. This description should work as a guideline for users to understand the output type, "
|
|
93
|
+
"and also can be used as a expansion specification for the type definition beyond the base "
|
|
94
|
+
"vocabulary."
|
|
95
|
+
)
|
|
79
96
|
properties: Dict[str, real_valued_primitives] = pydantic.Field(
|
|
80
97
|
{},
|
|
81
|
-
description="(optional) Specification for type properties, if any."
|
|
98
|
+
description="(optional) Specification for type properties, if any. ``\"*\"`` indicates any value."
|
|
82
99
|
)
|
|
83
100
|
|
|
84
101
|
@pydantic.validator('at_type', pre=True)
|
|
@@ -91,18 +108,24 @@ class Output(_BaseModel):
|
|
|
91
108
|
title = 'CLAMS Output Specification'
|
|
92
109
|
extra = 'forbid'
|
|
93
110
|
allow_population_by_field_name = True
|
|
111
|
+
|
|
112
|
+
def add_description(self, description: str):
|
|
113
|
+
"""
|
|
114
|
+
Add description string to the type specification.
|
|
115
|
+
"""
|
|
116
|
+
self.description = description
|
|
94
117
|
|
|
95
118
|
|
|
96
119
|
class Input(Output):
|
|
97
120
|
"""
|
|
98
121
|
Data model that describes input specification of a CLAMS app.
|
|
99
122
|
|
|
100
|
-
CLAMS apps are expected to have at least one
|
|
101
|
-
be defined by a ``@type`` URI string. If the type has specific properties and values,
|
|
123
|
+
CLAMS apps are expected to have at least one input type, and each type must
|
|
124
|
+
be defined by a ``@type`` URI string. If the type has specific properties and values required by the app,
|
|
102
125
|
they can be described in the (optional) ``properties`` field. Finally, a human-readable
|
|
103
126
|
verbose description can be provided in the (optional) ``description`` field for users.
|
|
104
127
|
|
|
105
|
-
Developers should take diligent care to include all
|
|
128
|
+
Developers should take diligent care to include all input types and their properties in the app metadata.
|
|
106
129
|
"""
|
|
107
130
|
required: bool = pydantic.Field(
|
|
108
131
|
None,
|
|
@@ -138,16 +161,33 @@ class RuntimeParameter(_BaseModel):
|
|
|
138
161
|
)
|
|
139
162
|
type: param_value_types = pydantic.Field(
|
|
140
163
|
...,
|
|
141
|
-
description=f"Type of the parameter value the app expects. Must be one of {param_value_types_values}."
|
|
164
|
+
description=f"Type of the parameter value the app expects. Must be one of {param_value_types_values}. When "
|
|
165
|
+
"type is ``map``, ``multivalued=true`` is automatically forced. \n\n"
|
|
166
|
+
"Notes for developers: \n\n"
|
|
167
|
+
"When the type is ``map``, the parameter value (still a single string from the users' perspective) "
|
|
168
|
+
"must be formatted as a ``KEY:VALUE`` pair, namely a colon-separated string. To pass multiple "
|
|
169
|
+
"key-value pairs, users need to pass the parameter multiple times (remember ``type=map`` implies "
|
|
170
|
+
"``multivalued=true``) with pairs in the colon-separated format. \n\n"
|
|
171
|
+
"Also, the `VALUE` part of the user input is always expected and handled as a string. If a "
|
|
172
|
+
"developers wants to do more text processing on the passed value to accept more complex data types "
|
|
173
|
+
"or structures (e.g., map from a string to a list of strings), it is up to the developer. However, "
|
|
174
|
+
"any additional form requirements should be precisely described in the ``description`` field for "
|
|
175
|
+
"users. \n\n"
|
|
176
|
+
"Finally, the same format is expected for the default value, if any. For example, if the default "
|
|
177
|
+
"desired dictionary is ``{'key1': 'value1', 'key2': 'value2'}``, the default value (used when "
|
|
178
|
+
"initializing a parameter) should be ``['key1:value1','key2:value2']``\n."
|
|
142
179
|
)
|
|
143
180
|
choices: List[real_valued_primitives] = pydantic.Field(
|
|
144
181
|
None,
|
|
145
182
|
description="(optional) List of string values that can be accepted."
|
|
146
183
|
)
|
|
147
|
-
default: real_valued_primitives = pydantic.Field(
|
|
184
|
+
default: Union[real_valued_primitives, List[real_valued_primitives]] = pydantic.Field(
|
|
148
185
|
None,
|
|
149
|
-
description="(optional) Default value for the parameter
|
|
150
|
-
"
|
|
186
|
+
description="(optional) Default value for the parameter.\n\n"
|
|
187
|
+
"Notes for developers: \n\n"
|
|
188
|
+
"Setting a default value makes a parameter `optional`. \n\n"
|
|
189
|
+
"When ``multivalued=True``, the default value should be a list of values. \n\n"
|
|
190
|
+
"When ``type=map``, the default value should be a list of colon-separated strings. \n\n"
|
|
151
191
|
)
|
|
152
192
|
multivalued: bool = pydantic.Field(
|
|
153
193
|
...,
|
|
@@ -157,8 +197,15 @@ class RuntimeParameter(_BaseModel):
|
|
|
157
197
|
|
|
158
198
|
def __init__(self, **kwargs):
|
|
159
199
|
super().__init__(**kwargs)
|
|
200
|
+
if self.type == 'map':
|
|
201
|
+
if self.multivalued is False:
|
|
202
|
+
raise ValueError("When the type is `map`, multivalued must not be False.")
|
|
203
|
+
else:
|
|
204
|
+
self.multivalued = True
|
|
160
205
|
if self.multivalued is None:
|
|
161
206
|
self.multivalued = False
|
|
207
|
+
if self.multivalued and self.default is not None and not isinstance(self.default, list):
|
|
208
|
+
self.default = [self.default]
|
|
162
209
|
|
|
163
210
|
class Config:
|
|
164
211
|
title = 'CLAMS App Runtime Parameter'
|
|
@@ -301,13 +348,14 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
301
348
|
return True
|
|
302
349
|
return False
|
|
303
350
|
|
|
304
|
-
def add_input(self, at_type: Union[str, vocabulary.ThingTypesBase], required: bool = True, **properties):
|
|
351
|
+
def add_input(self, at_type: Union[str, vocabulary.ThingTypesBase], required: bool = True, **properties) -> Input:
|
|
305
352
|
"""
|
|
306
353
|
Helper method to add an element to the ``input`` list.
|
|
307
354
|
|
|
308
355
|
:param at_type: ``@type`` of the input object
|
|
309
356
|
:param required: whether this type is mandatory or optional
|
|
310
357
|
:param properties: additional property specifications
|
|
358
|
+
:return: the newly added Input object
|
|
311
359
|
"""
|
|
312
360
|
new = Input(at_type=at_type, required=required)
|
|
313
361
|
if len(properties) > 0:
|
|
@@ -320,8 +368,9 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
320
368
|
# TODO (krim @ 5/12/21): automatically add *optional* input types to parameter
|
|
321
369
|
# see https://github.com/clamsproject/clams-python/issues/29 for discussion
|
|
322
370
|
pass
|
|
371
|
+
return new
|
|
323
372
|
|
|
324
|
-
def add_input_oneof(self, *inputs: Union[str, Input, vocabulary.ThingTypesBase]
|
|
373
|
+
def add_input_oneof(self, *inputs: Union[str, Input, vocabulary.ThingTypesBase]):
|
|
325
374
|
newinputs = []
|
|
326
375
|
if len(inputs) == 1:
|
|
327
376
|
if isinstance(inputs[0], Input):
|
|
@@ -342,12 +391,13 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
342
391
|
newinputs.append(i)
|
|
343
392
|
self.input.append(newinputs)
|
|
344
393
|
|
|
345
|
-
def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **properties):
|
|
394
|
+
def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **properties) -> Output:
|
|
346
395
|
"""
|
|
347
396
|
Helper method to add an element to the ``output`` list.
|
|
348
397
|
|
|
349
398
|
:param at_type: ``@type`` of the input object
|
|
350
399
|
:param properties: additional property specifications
|
|
400
|
+
:return: the newly added Output object
|
|
351
401
|
"""
|
|
352
402
|
new = Output(at_type=at_type)
|
|
353
403
|
if len(properties) > 0:
|
|
@@ -356,21 +406,31 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
356
406
|
self.output.append(new)
|
|
357
407
|
else:
|
|
358
408
|
raise ValueError(f"Cannot add a duplicate output '{new}'.")
|
|
409
|
+
return new
|
|
359
410
|
|
|
360
411
|
def add_parameter(self, name: str, description: str, type: param_value_types,
|
|
361
412
|
choices: Optional[List[real_valued_primitives]] = None,
|
|
362
413
|
multivalued: bool = False,
|
|
363
|
-
default: real_valued_primitives = None):
|
|
414
|
+
default: Union[real_valued_primitives, List[real_valued_primitives]] = None):
|
|
364
415
|
"""
|
|
365
416
|
Helper method to add an element to the ``parameters`` list.
|
|
366
417
|
"""
|
|
367
|
-
# casting default values (when the value is not nothing)
|
|
368
|
-
# the values are correctly
|
|
418
|
+
# casting default values (when the value is not nothing) to str
|
|
419
|
+
# will make sure the values are correctly handled by the pydantic
|
|
369
420
|
# see https://docs.pydantic.dev/1.10/usage/types/#unions
|
|
370
421
|
# e.g. casting 0.1 using the `primitives` dict will result in 0 (int)
|
|
371
422
|
# while casting "0.1" using the `primitives` dict will result in 0.1 (float)
|
|
372
|
-
|
|
373
|
-
|
|
423
|
+
if type == 'map' and multivalued is False:
|
|
424
|
+
multivalued = True
|
|
425
|
+
if default is not None:
|
|
426
|
+
if isinstance(default, list):
|
|
427
|
+
default = [str(d) for d in default]
|
|
428
|
+
else:
|
|
429
|
+
default = str(default)
|
|
430
|
+
|
|
431
|
+
new_param = RuntimeParameter(
|
|
432
|
+
name=name, description=description, type=type, choices=choices, multivalued=multivalued,
|
|
433
|
+
default=default)
|
|
374
434
|
if new_param.name not in [param.name for param in self.parameters]:
|
|
375
435
|
self.parameters.append(new_param)
|
|
376
436
|
else:
|
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
from typing import Dict, Union, Any, Tuple, List
|
|
2
|
-
|
|
3
1
|
import jsonschema
|
|
4
2
|
from flask import Flask, request, Response
|
|
5
3
|
from flask_restful import Resource, Api
|
|
6
4
|
from mmif import Mmif
|
|
7
5
|
|
|
8
6
|
from clams.app import ClamsApp
|
|
9
|
-
from clams.appmetadata import real_valued_primitives
|
|
10
7
|
|
|
11
8
|
|
|
12
9
|
class Restifier(object):
|
|
@@ -107,8 +104,6 @@ class ClamsHTTPApi(Resource):
|
|
|
107
104
|
def __init__(self, cla_instance: ClamsApp):
|
|
108
105
|
super().__init__()
|
|
109
106
|
self.cla = cla_instance
|
|
110
|
-
self.metadata_param_caster = ParameterCaster(self.cla.metadata_param_spec) # pytype: disable=wrong-arg-types
|
|
111
|
-
self.annotate_param_caster = ParameterCaster(self.cla.annotate_param_spec) # pytype: disable=wrong-arg-types
|
|
112
107
|
|
|
113
108
|
@staticmethod
|
|
114
109
|
def json_to_response(json_str: str, status=200) -> Response:
|
|
@@ -129,7 +124,7 @@ class ClamsHTTPApi(Resource):
|
|
|
129
124
|
|
|
130
125
|
:return: Returns app metadata in a HTTP response.
|
|
131
126
|
"""
|
|
132
|
-
return self.json_to_response(self.cla.appmetadata(**
|
|
127
|
+
return self.json_to_response(self.cla.appmetadata(**request.args))
|
|
133
128
|
|
|
134
129
|
def post(self) -> Response:
|
|
135
130
|
"""
|
|
@@ -138,93 +133,16 @@ class ClamsHTTPApi(Resource):
|
|
|
138
133
|
|
|
139
134
|
:return: Returns MMIF output from a ClamsApp in a HTTP response.
|
|
140
135
|
"""
|
|
141
|
-
raw_data = request.get_data()
|
|
136
|
+
raw_data = request.get_data().decode('utf-8')
|
|
142
137
|
# this will catch duplicate arguments with different values into a list under the key
|
|
143
|
-
raw_params = request.args.to_dict(False)
|
|
138
|
+
raw_params = request.args.to_dict(flat=False)
|
|
144
139
|
try:
|
|
145
140
|
_ = Mmif(raw_data)
|
|
146
141
|
except jsonschema.exceptions.ValidationError as e:
|
|
147
142
|
return Response(response="Invalid input data. See below for validation error.\n\n" + str(e), status=500, mimetype='text/plain')
|
|
148
143
|
try:
|
|
149
|
-
return self.json_to_response(self.cla.annotate(raw_data, **
|
|
150
|
-
except Exception
|
|
151
|
-
return self.json_to_response(self.cla.record_error(raw_data,
|
|
144
|
+
return self.json_to_response(self.cla.annotate(raw_data, **raw_params))
|
|
145
|
+
except Exception:
|
|
146
|
+
return self.json_to_response(self.cla.record_error(raw_data, **raw_params).serialize(pretty=True), status=500)
|
|
152
147
|
|
|
153
148
|
put = post
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
class ParameterCaster(object):
|
|
157
|
-
"""
|
|
158
|
-
A helper class to convert parameters passed by HTTP query strings to
|
|
159
|
-
proper python data types.
|
|
160
|
-
|
|
161
|
-
:param param_spec: A specification of a data types of parameters
|
|
162
|
-
"""
|
|
163
|
-
def __init__(self, param_spec: Dict[str, Tuple[real_valued_primitives, bool]]):
|
|
164
|
-
self.param_spec = param_spec
|
|
165
|
-
|
|
166
|
-
def cast(self, args: Dict[str, List[str]]) -> Dict[str, Union[real_valued_primitives, List[real_valued_primitives]]]:
|
|
167
|
-
"""
|
|
168
|
-
Given parameter specification, tries to cast values of args to specified Python data types.
|
|
169
|
-
Note that this caster deals with query strings, thus all keys and values in the input args are plain strings.
|
|
170
|
-
Also note that the caster does not handle "unexpected" parameters came as an input.
|
|
171
|
-
Handling (raising an exception or issuing a warning upon receiving) an unexpected runtime parameter
|
|
172
|
-
must be done within the app itself.
|
|
173
|
-
Thus, when a key is not found in the parameter specifications, it should just pass it as a vanilla string.
|
|
174
|
-
|
|
175
|
-
:param args: k-v pairs
|
|
176
|
-
:return: A new dictionary of type-casted args
|
|
177
|
-
"""
|
|
178
|
-
casted = {}
|
|
179
|
-
for k, vs in args.items():
|
|
180
|
-
if k in self.param_spec:
|
|
181
|
-
for v in vs:
|
|
182
|
-
type_, multivalued = self.param_spec[k]
|
|
183
|
-
if multivalued or k not in casted: # effectively only keeps the first value for non-multi params
|
|
184
|
-
if type_ == bool:
|
|
185
|
-
v = self.bool_param(v)
|
|
186
|
-
elif type_ == float:
|
|
187
|
-
v = self.float_param(v)
|
|
188
|
-
elif type_ == int:
|
|
189
|
-
v = self.int_param(v)
|
|
190
|
-
elif type_ == str:
|
|
191
|
-
v = self.str_param(v)
|
|
192
|
-
if multivalued:
|
|
193
|
-
casted.setdefault(k, []).append(v)
|
|
194
|
-
else:
|
|
195
|
-
casted[k] = v
|
|
196
|
-
else:
|
|
197
|
-
if len(vs) > 1:
|
|
198
|
-
casted[k] = vs
|
|
199
|
-
else:
|
|
200
|
-
casted[k] = vs[0]
|
|
201
|
-
|
|
202
|
-
return casted
|
|
203
|
-
|
|
204
|
-
@staticmethod
|
|
205
|
-
def bool_param(value):
|
|
206
|
-
"""
|
|
207
|
-
Helper function to convert string values to bool type.
|
|
208
|
-
"""
|
|
209
|
-
return False if value in (False, 0, 'False', 'false', '0') else True
|
|
210
|
-
|
|
211
|
-
@staticmethod
|
|
212
|
-
def float_param(value):
|
|
213
|
-
"""
|
|
214
|
-
Helper function to convert string values to float type.
|
|
215
|
-
"""
|
|
216
|
-
return float(value)
|
|
217
|
-
|
|
218
|
-
@staticmethod
|
|
219
|
-
def int_param(value):
|
|
220
|
-
"""
|
|
221
|
-
Helper function to convert string values to int type.
|
|
222
|
-
"""
|
|
223
|
-
return int(value)
|
|
224
|
-
|
|
225
|
-
@staticmethod
|
|
226
|
-
def str_param(value):
|
|
227
|
-
"""
|
|
228
|
-
Helper function to convert string values to string type.
|
|
229
|
-
"""
|
|
230
|
-
return value
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.1.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clams-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.5
|
|
4
4
|
Summary: A collection of APIs to develop CLAMS app for python
|
|
5
5
|
Home-page: https://clams.ai
|
|
6
6
|
Author: Brandeis Lab for Linguistics and Computation
|
|
@@ -14,7 +14,7 @@ Classifier: Programming Language :: Python :: 3 :: Only
|
|
|
14
14
|
Requires-Python: >=3.8
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
License-File: LICENSE
|
|
17
|
-
Requires-Dist: mmif-python==1.0.
|
|
17
|
+
Requires-Dist: mmif-python==1.0.11
|
|
18
18
|
Requires-Dist: Flask>=2
|
|
19
19
|
Requires-Dist: Flask-RESTful>=0.3.9
|
|
20
20
|
Requires-Dist: gunicorn>=20
|
|
@@ -13,8 +13,8 @@ from mmif import Mmif, Document, DocumentTypes, AnnotationTypes, View, __specver
|
|
|
13
13
|
|
|
14
14
|
import clams.app
|
|
15
15
|
import clams.restify
|
|
16
|
+
from clams.app import ParameterCaster
|
|
16
17
|
from clams.appmetadata import AppMetadata, Input
|
|
17
|
-
from clams.restify import ParameterCaster
|
|
18
18
|
|
|
19
19
|
|
|
20
20
|
class ExampleInputMMIF(object):
|
|
@@ -123,7 +123,7 @@ class TestClamsApp(unittest.TestCase):
|
|
|
123
123
|
self.assertTrue('properties' not in elem)
|
|
124
124
|
self.assertTrue(elem['required'])
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
def test_metadata_inputoutput(self):
|
|
127
127
|
self.app.metadata.add_output(AnnotationTypes.BoundingBox, boxType='text')
|
|
128
128
|
metadata = json.loads(self.app.appmetadata())
|
|
129
129
|
self.assertEqual(len(metadata['output']), 2)
|
|
@@ -132,11 +132,22 @@ class TestClamsApp(unittest.TestCase):
|
|
|
132
132
|
self.app.metadata.add_input(at_type=DocumentTypes.TextDocument)
|
|
133
133
|
with self.assertRaises(ValueError):
|
|
134
134
|
self.app.metadata.add_output(AnnotationTypes.BoundingBox, boxType='text')
|
|
135
|
-
# but this should
|
|
136
|
-
self.app.metadata.add_output(AnnotationTypes.BoundingBox, boxType='face')
|
|
135
|
+
# but this should, even when there's `BoundingBox` already because the boxType prop value is different
|
|
136
|
+
face_bb_out = self.app.metadata.add_output(AnnotationTypes.BoundingBox, boxType='face')
|
|
137
|
+
# with an optional "comment"
|
|
138
|
+
face_bb_out.description = 'face bounding box'
|
|
139
|
+
# then should be able to set the description of the existing output
|
|
140
|
+
text_bb_out = next(o for o in self.app.metadata.output if o.properties.get('boxType') == 'text')
|
|
141
|
+
text_bb_out.description = 'text bounding box'
|
|
137
142
|
metadata = json.loads(self.app.appmetadata())
|
|
138
143
|
self.assertEqual(len(metadata['input']), 2)
|
|
139
|
-
|
|
144
|
+
for i in metadata['input']:
|
|
145
|
+
self.assertTrue('properties' not in i)
|
|
146
|
+
self.assertTrue('description' not in i)
|
|
147
|
+
for o in metadata['output']:
|
|
148
|
+
if o['@type'] == AnnotationTypes.BoundingBox:
|
|
149
|
+
self.assertTrue('properties' in o)
|
|
150
|
+
self.assertTrue('description' in o)
|
|
140
151
|
# adding input with properties
|
|
141
152
|
self.app.metadata.add_input(at_type=AnnotationTypes.TimeFrame, frameType='speech')
|
|
142
153
|
metadata = json.loads(self.app.appmetadata())
|
|
@@ -151,19 +162,42 @@ class TestClamsApp(unittest.TestCase):
|
|
|
151
162
|
j = Input(at_type=AnnotationTypes.VideoObject)
|
|
152
163
|
with self.assertRaises(ValueError):
|
|
153
164
|
self.app.metadata.add_input_oneof(i, j)
|
|
165
|
+
|
|
166
|
+
def test_metadata_runtimeparams(self):
|
|
154
167
|
# now parameters
|
|
168
|
+
num_params = 1 # starts with `raise_error` in metadata.py
|
|
155
169
|
# using a custom class
|
|
156
170
|
# this should conflict with existing parameter
|
|
157
171
|
with self.assertRaises(ValueError):
|
|
158
172
|
self.app.metadata.add_parameter(
|
|
159
173
|
name='raise_error', description='force raise a ValueError',
|
|
160
174
|
type='boolean', default='false')
|
|
161
|
-
|
|
175
|
+
num_params += 0
|
|
176
|
+
|
|
177
|
+
# some random multiple choice parameters
|
|
162
178
|
self.app.metadata.add_parameter(
|
|
163
|
-
name='
|
|
179
|
+
name='single_choice', description='meaningless choice for a single value',
|
|
180
|
+
type='integer', choices=[1, 2, 3, 4, 5], default='3', multivalued=False)
|
|
181
|
+
num_params += 1
|
|
182
|
+
|
|
183
|
+
self.app.metadata.add_parameter(
|
|
184
|
+
name='multiple_choice', description='meaningless choice for any number of values',
|
|
164
185
|
type='integer', choices=[1, 2, 3, 4, 5], default=3, multivalued=True)
|
|
186
|
+
num_params += 1
|
|
187
|
+
|
|
165
188
|
metadata = json.loads(self.app.appmetadata())
|
|
166
|
-
self.assertEqual(len(metadata['parameters']),
|
|
189
|
+
self.assertEqual(len(metadata['parameters']), num_params + len(self.app.universal_parameters))
|
|
190
|
+
|
|
191
|
+
for p in metadata['parameters']:
|
|
192
|
+
# default value must match the data type
|
|
193
|
+
if p['name'] == 'single_choice':
|
|
194
|
+
self.assertFalse(p['multivalued'])
|
|
195
|
+
self.assertTrue(isinstance(p['default'], int))
|
|
196
|
+
# default for multivalued para must be a list
|
|
197
|
+
if p['name'] == 'multiple_choice':
|
|
198
|
+
self.assertTrue(p['multivalued'])
|
|
199
|
+
self.assertTrue(isinstance(p['default'], list))
|
|
200
|
+
self.assertEqual(len(p['default']), 1)
|
|
167
201
|
# now more additional metadata
|
|
168
202
|
self.app.metadata.add_more('one', 'more')
|
|
169
203
|
self.assertEqual(self.app.metadata.more['one'], 'more')
|
|
@@ -173,7 +207,7 @@ class TestClamsApp(unittest.TestCase):
|
|
|
173
207
|
self.app.metadata.add_more('one', '')
|
|
174
208
|
|
|
175
209
|
# finally for an eye exam
|
|
176
|
-
print(self.app.appmetadata(pretty=
|
|
210
|
+
print(self.app.appmetadata(pretty=['true']))
|
|
177
211
|
|
|
178
212
|
@pytest.mark.skip('legacy type version check')
|
|
179
213
|
def test__check_mmif_compatibility(self):
|
|
@@ -205,7 +239,7 @@ class TestClamsApp(unittest.TestCase):
|
|
|
205
239
|
for v in out_mmif.views:
|
|
206
240
|
if v.metadata.app == self.app.metadata.identifier:
|
|
207
241
|
self.assertEqual(len(v.metadata.parameters), 0) # no params were passed when `annotate()` was called
|
|
208
|
-
out_mmif = self.app.annotate(self.in_mmif, pretty=False)
|
|
242
|
+
out_mmif = self.app.annotate(self.in_mmif, pretty=[str(False)])
|
|
209
243
|
out_mmif = Mmif(out_mmif)
|
|
210
244
|
for v in out_mmif.views:
|
|
211
245
|
if v.metadata.app == self.app.metadata.identifier:
|
|
@@ -226,8 +260,6 @@ class TestClamsApp(unittest.TestCase):
|
|
|
226
260
|
self.app._annotate = MagicMock(return_value=m)
|
|
227
261
|
with self.assertRaises(jsonschema.ValidationError):
|
|
228
262
|
self.app.annotate(self.in_mmif)
|
|
229
|
-
|
|
230
|
-
|
|
231
263
|
|
|
232
264
|
def test_open_document_location(self):
|
|
233
265
|
mmif = ExampleInputMMIF.get_rawmmif()
|
|
@@ -242,40 +274,54 @@ class TestClamsApp(unittest.TestCase):
|
|
|
242
274
|
|
|
243
275
|
def test_refine_parameters(self):
|
|
244
276
|
self.app.metadata.parameters = []
|
|
245
|
-
self.app.metadata.add_parameter('param1', '
|
|
246
|
-
|
|
247
|
-
self.app.metadata.add_parameter('
|
|
248
|
-
|
|
249
|
-
self.app.metadata.add_parameter('
|
|
250
|
-
|
|
277
|
+
self.app.metadata.add_parameter('param1', type='string',
|
|
278
|
+
description='string -def +req')
|
|
279
|
+
self.app.metadata.add_parameter('param2', type='string', default='second_default',
|
|
280
|
+
description='stirng +def')
|
|
281
|
+
self.app.metadata.add_parameter('param3', type='boolean', default='f',
|
|
282
|
+
description='bool +def')
|
|
283
|
+
self.app.metadata.add_parameter('param4', type='integer', default=1, choices="1 2 3".split(),
|
|
284
|
+
description='int +def +choices')
|
|
285
|
+
self.app.metadata.add_parameter('param5', type='number', default=0.5,
|
|
286
|
+
description='float +def')
|
|
287
|
+
self.app.metadata.add_parameter('param6', type='number', multivalued=True, default=[0.5],
|
|
288
|
+
description='float +def +mv')
|
|
289
|
+
self.app.metadata.add_parameter('param7', type='number', default=[], multivalued=True,
|
|
290
|
+
description='float +def +empty +mv')
|
|
291
|
+
with self.assertRaises(ValueError):
|
|
292
|
+
self.app._refine_params(**{}) # param1 is required, but not passed
|
|
293
|
+
dummy_params = {'param1': ['okay'], 'non_parameter': ['should be ignored']}
|
|
251
294
|
conf = self.app._refine_params(**dummy_params)
|
|
252
295
|
# should not refine what's already refined
|
|
253
296
|
double_refine = self.app._refine_params(**conf)
|
|
254
297
|
self.assertTrue(clams.ClamsApp._RAW_PARAMS_KEY in double_refine)
|
|
255
298
|
self.assertEqual(double_refine, conf)
|
|
256
299
|
conf.pop(clams.ClamsApp._RAW_PARAMS_KEY, None)
|
|
257
|
-
self.assertEqual(len(conf),
|
|
300
|
+
self.assertEqual(len(conf), 7) # 1 from `param1`, 6 from default value
|
|
258
301
|
self.assertFalse('non_parameter' in conf)
|
|
259
302
|
self.assertEqual(type(conf['param1']), str)
|
|
260
303
|
self.assertEqual(type(conf['param2']), str)
|
|
261
304
|
self.assertEqual(type(conf['param3']), bool)
|
|
262
305
|
self.assertEqual(type(conf['param4']), int)
|
|
263
306
|
self.assertEqual(type(conf['param5']), float)
|
|
307
|
+
self.assertEqual(type(conf['param6']), list)
|
|
308
|
+
self.assertEqual(type(conf['param7']), list)
|
|
309
|
+
self.assertEqual(len(conf['param7']), 0)
|
|
264
310
|
with self.assertRaises(ValueError):
|
|
265
311
|
# because param1 doesn't have a default value and thus a required param
|
|
266
|
-
self.app._refine_params(param2='okay')
|
|
267
|
-
with self.
|
|
312
|
+
self.app._refine_params(param2=['okay'])
|
|
313
|
+
with self.assertRaisesRegex(ValueError, r'.+must be one of.+'):
|
|
268
314
|
# because param4 can't be 4, note that param1 is "required"
|
|
269
|
-
self.app._refine_params(param1='p1', param4=4)
|
|
315
|
+
self.app._refine_params(param1=['p1'], param4=['4'])
|
|
270
316
|
|
|
271
317
|
def test_error_handling(self):
|
|
272
|
-
params = {'raise_error':
|
|
318
|
+
params = {'raise_error': 'true', 'pretty': 'true'}
|
|
273
319
|
in_mmif = Mmif(self.in_mmif)
|
|
274
320
|
try:
|
|
275
321
|
out_mmif = self.app.annotate(in_mmif, **params)
|
|
276
322
|
except Exception as e:
|
|
277
|
-
out_mmif_from_str = self.app.set_error_view(self.in_mmif, params)
|
|
278
|
-
out_mmif_from_mmif = self.app.set_error_view(in_mmif, params)
|
|
323
|
+
out_mmif_from_str = self.app.set_error_view(self.in_mmif, **params)
|
|
324
|
+
out_mmif_from_mmif = self.app.set_error_view(in_mmif, **params)
|
|
279
325
|
self.assertEqual(out_mmif_from_mmif.views, out_mmif_from_str.views)
|
|
280
326
|
out_mmif = out_mmif_from_str
|
|
281
327
|
self.assertIsNotNone(out_mmif)
|
|
@@ -374,13 +420,14 @@ class TestRestifier(unittest.TestCase):
|
|
|
374
420
|
class TestParameterCaster(unittest.TestCase):
|
|
375
421
|
|
|
376
422
|
def setUp(self) -> None:
|
|
377
|
-
self.param_spec = {'str_param': (
|
|
378
|
-
'number_param': (
|
|
379
|
-
'int_param': (
|
|
380
|
-
'bool_param': (
|
|
381
|
-
'str_multi_param': (
|
|
423
|
+
self.param_spec = {'str_param': ('string', False),
|
|
424
|
+
'number_param': ('number', False),
|
|
425
|
+
'int_param': ('integer', False),
|
|
426
|
+
'bool_param': ('boolean', False),
|
|
427
|
+
'str_multi_param': ('string', True),
|
|
428
|
+
'map_param': ('map', True),
|
|
382
429
|
}
|
|
383
|
-
|
|
430
|
+
|
|
384
431
|
def test_cast(self):
|
|
385
432
|
caster = ParameterCaster(self.param_spec)
|
|
386
433
|
params = {
|
|
@@ -391,7 +438,9 @@ class TestParameterCaster(unittest.TestCase):
|
|
|
391
438
|
'str_multi_param': ['value1', 'value2'],
|
|
392
439
|
'undefined_param_single': ['undefined_value1'],
|
|
393
440
|
'undefined_param_multi': ['undefined_value1', 'undefined_value2'],
|
|
441
|
+
'map_param': ['key1:val1', 'key2:val2', 'key3:val3', 'key1:val4']
|
|
394
442
|
}
|
|
443
|
+
# all "RAW" parameter values are assumed to be strings
|
|
395
444
|
self.assertTrue(all(map(lambda x: isinstance(x, str), itertools.chain.from_iterable(params.values()))))
|
|
396
445
|
casted = caster.cast(params)
|
|
397
446
|
# must push out to the list
|
|
@@ -404,6 +453,11 @@ class TestParameterCaster(unittest.TestCase):
|
|
|
404
453
|
self.assertTrue('undefined_param_multi' in casted)
|
|
405
454
|
self.assertFalse(isinstance(casted['undefined_param_single'], list))
|
|
406
455
|
self.assertTrue(isinstance(casted['undefined_param_multi'], list))
|
|
456
|
+
self.assertEqual(3, len(casted['map_param']))
|
|
457
|
+
self.assertTrue(all(map(lambda x: isinstance(x, str), casted['map_param'].values())))
|
|
458
|
+
self.assertTrue('val4', len(casted['map_param']['key1']))
|
|
459
|
+
self.assertTrue('val2', len(casted['map_param']['key2']))
|
|
460
|
+
self.assertTrue('val3', len(casted['map_param']['key3']))
|
|
407
461
|
unknown_param_key = 'unknown'
|
|
408
462
|
unknown_param_val = 'dunno'
|
|
409
463
|
params[unknown_param_key] = [unknown_param_val]
|
clams-python-1.1.3/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.1.3
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/.dockerignore.template
RENAMED
|
File without changes
|
|
File without changes
|
{clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/Containerfile.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/app/requirements.txt.template
RENAMED
|
File without changes
|
{clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/for-clams-team.md.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clams-python-1.1.3 → clams-python-1.1.5}/clams/develop/templates/gha/workflows/publish.yml.template
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|