clams-python 1.1.2__tar.gz → 1.1.4__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.2/clams_python.egg-info → clams-python-1.1.4}/PKG-INFO +2 -2
- clams-python-1.1.4/VERSION +1 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/app/__init__.py +148 -34
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/appmetadata/__init__.py +90 -21
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/restify/__init__.py +6 -88
- clams-python-1.1.4/clams/ver/__init__.py +1 -0
- {clams-python-1.1.2 → clams-python-1.1.4/clams_python.egg-info}/PKG-INFO +2 -2
- {clams-python-1.1.2 → clams-python-1.1.4}/clams_python.egg-info/requires.txt +1 -1
- {clams-python-1.1.2 → clams-python-1.1.4}/requirements.txt +1 -1
- {clams-python-1.1.2 → clams-python-1.1.4}/tests/test_clamsapp.py +90 -34
- clams-python-1.1.2/VERSION +0 -1
- clams-python-1.1.2/clams/ver/__init__.py +0 -1
- {clams-python-1.1.2 → clams-python-1.1.4}/LICENSE +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/MANIFEST.in +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/README.md +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/__init__.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/__init__.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/.dockerignore.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/.gitignore.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/Containerfile.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/LICENSE.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/README.md.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/app.py.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/metadata.py.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/requirements.txt.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/gha/for-clams-team.md.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/gha/workflows/issue-apps-project.yml.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/gha/workflows/issue-assign.yml.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/gha/workflows/issue-close.yml.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/gha/workflows/publish.yml.template +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/mmif_utils/__init__.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/mmif_utils/rewind.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/mmif_utils/source.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams/serve/__init__.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams_python.egg-info/SOURCES.txt +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams_python.egg-info/dependency_links.txt +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams_python.egg-info/entry_points.txt +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/clams_python.egg-info/top_level.txt +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/setup.cfg +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/setup.py +0 -0
- {clams-python-1.1.2 → clams-python-1.1.4}/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.4
|
|
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.4
|
|
@@ -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, Iterable
|
|
13
13
|
|
|
14
14
|
from mmif import Mmif, Document, DocumentTypes, View
|
|
15
|
-
from clams.appmetadata import AppMetadata
|
|
15
|
+
from clams.appmetadata import AppMetadata, RuntimeParameter, real_valued_primitives
|
|
16
16
|
|
|
17
17
|
logging.basicConfig(
|
|
18
18
|
level=logging.WARNING,
|
|
@@ -29,10 +29,10 @@ class ClamsApp(ABC):
|
|
|
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
31
|
universal_parameters = [
|
|
32
|
-
{
|
|
32
|
+
RuntimeParameter(**{
|
|
33
33
|
'name': 'pretty', 'type': 'boolean', 'choices': None, 'default': False, 'multivalued': False,
|
|
34
34
|
'description': 'The JSON body of the HTTP response will be re-formatted with 2-space indentation',
|
|
35
|
-
},
|
|
35
|
+
}),
|
|
36
36
|
]
|
|
37
37
|
# this key is used to store users' raw input params in the parameter dict
|
|
38
38
|
# even after "refinement" (i.e., casting to proper data types)
|
|
@@ -42,24 +42,22 @@ class ClamsApp(ABC):
|
|
|
42
42
|
self.metadata: AppMetadata = self._load_appmetadata()
|
|
43
43
|
super().__init__()
|
|
44
44
|
# data type specification for common parameters
|
|
45
|
-
python_type = {"boolean": bool, "number": float, "integer": int, "string": str}
|
|
46
45
|
|
|
47
|
-
self.metadata_param_spec = {}
|
|
48
|
-
self.annotate_param_spec = {}
|
|
49
46
|
for param in ClamsApp.universal_parameters:
|
|
50
|
-
self.metadata.
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
self.annotate_param_spec[param_spec.name] = (python_type[param_spec.type], param_spec.multivalued)
|
|
47
|
+
self.metadata.parameters.append(param)
|
|
48
|
+
self.metadata_param_caster = ParameterCaster(ClamsApp.universal_parameters) # pytype: disable=wrong-arg-types
|
|
49
|
+
self.annotate_param_caster = ParameterCaster(self.metadata.parameters) # pytype: disable=wrong-arg-types
|
|
54
50
|
self.logger = logging.getLogger(self.metadata.identifier)
|
|
55
51
|
|
|
56
|
-
def appmetadata(self, **kwargs) -> str:
|
|
52
|
+
def appmetadata(self, **kwargs: List[str]) -> str:
|
|
57
53
|
"""
|
|
58
54
|
A public method to get metadata for this app as a string.
|
|
59
55
|
|
|
60
56
|
:return: Serialized JSON string of the metadata
|
|
61
57
|
"""
|
|
62
|
-
|
|
58
|
+
# cast only, no refinement
|
|
59
|
+
casted = self.metadata_param_caster.cast(kwargs)
|
|
60
|
+
pretty = casted.pop('pretty') if 'pretty' in casted else False
|
|
63
61
|
return self.metadata.jsonify(pretty)
|
|
64
62
|
|
|
65
63
|
def _load_appmetadata(self) -> AppMetadata:
|
|
@@ -98,7 +96,7 @@ class ClamsApp(ABC):
|
|
|
98
96
|
def _check_mmif_compatibility(target_specver, input_specver):
|
|
99
97
|
return target_specver.split('.')[:2] == input_specver.split('.')[:2]
|
|
100
98
|
|
|
101
|
-
def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params) -> str:
|
|
99
|
+
def annotate(self, mmif: Union[str, dict, Mmif], **runtime_params: List[str]) -> str:
|
|
102
100
|
"""
|
|
103
101
|
A public method to invoke the primary app function. It's essentially a
|
|
104
102
|
wrapper around :meth:`~clams.app.ClamsApp._annotate` method where some common operations
|
|
@@ -108,21 +106,22 @@ class ClamsApp(ABC):
|
|
|
108
106
|
:param runtime_params: An arbitrary set of k-v pairs to configure the app at runtime
|
|
109
107
|
:return: Serialized JSON string of the output of the app
|
|
110
108
|
"""
|
|
111
|
-
pretty = runtime_params.get('pretty', False)
|
|
112
109
|
if not isinstance(mmif, Mmif):
|
|
113
110
|
mmif = Mmif(mmif)
|
|
114
111
|
issued_warnings = []
|
|
115
112
|
for key in runtime_params:
|
|
116
|
-
if key not in self.
|
|
113
|
+
if key not in self.annotate_param_caster.param_spec:
|
|
117
114
|
issued_warnings.append(UserWarning(f'An undefined parameter "{key}" (value: "{runtime_params[key]}") is passed'))
|
|
118
|
-
|
|
115
|
+
# this will do casting + refinement altogether
|
|
116
|
+
refined = self._refine_params(**runtime_params)
|
|
117
|
+
pretty = refined.get('pretty', False)
|
|
119
118
|
with warnings.catch_warnings(record=True) as ws:
|
|
120
|
-
annotated = self._annotate(mmif, **
|
|
119
|
+
annotated = self._annotate(mmif, **refined)
|
|
121
120
|
if ws:
|
|
122
121
|
issued_warnings.extend(ws)
|
|
123
122
|
if issued_warnings:
|
|
124
123
|
warnings_view = annotated.new_view()
|
|
125
|
-
self.sign_view(warnings_view,
|
|
124
|
+
self.sign_view(warnings_view, runtime_params)
|
|
126
125
|
warnings_view.metadata.warnings = issued_warnings
|
|
127
126
|
return annotated.serialize(pretty=pretty, sanitize=True)
|
|
128
127
|
|
|
@@ -148,7 +147,7 @@ class ClamsApp(ABC):
|
|
|
148
147
|
"""
|
|
149
148
|
raise NotImplementedError()
|
|
150
149
|
|
|
151
|
-
def _refine_params(self, **runtime_params):
|
|
150
|
+
def _refine_params(self, **runtime_params: List[str]):
|
|
152
151
|
"""
|
|
153
152
|
Method to "fill" the parameter dictionary with default values, when a key-value is not specified in the input.
|
|
154
153
|
The input map is not really "filled" as a copy of it is returned with addition of default values.
|
|
@@ -159,19 +158,21 @@ class ClamsApp(ABC):
|
|
|
159
158
|
if self._RAW_PARAMS_KEY in runtime_params:
|
|
160
159
|
# meaning the dict is already refined, just return it
|
|
161
160
|
return runtime_params
|
|
162
|
-
|
|
161
|
+
refined = {}
|
|
162
|
+
|
|
163
|
+
casted = self.annotate_param_caster.cast(runtime_params)
|
|
163
164
|
for parameter in self.metadata.parameters:
|
|
164
|
-
if parameter.name in
|
|
165
|
-
if parameter.choices and
|
|
165
|
+
if parameter.name in casted:
|
|
166
|
+
if parameter.choices and casted[parameter.name] not in parameter.choices:
|
|
166
167
|
raise ValueError(f"Value for parameter \"{parameter.name}\" must be one of {parameter.choices}.")
|
|
167
|
-
|
|
168
|
+
refined[parameter.name] = casted[parameter.name]
|
|
168
169
|
elif parameter.default is not None:
|
|
169
|
-
|
|
170
|
+
refined[parameter.name] = parameter.default
|
|
170
171
|
else:
|
|
171
172
|
raise ValueError(f"Cannot find configuration for a required parameter \"{parameter.name}\".")
|
|
172
173
|
# raw input params are hidden under a special key
|
|
173
|
-
|
|
174
|
-
return
|
|
174
|
+
refined[self._RAW_PARAMS_KEY] = runtime_params
|
|
175
|
+
return refined
|
|
175
176
|
|
|
176
177
|
def get_configuration(self, **runtime_params):
|
|
177
178
|
warnings.warn("ClamsApp.get_configuration() is deprecated. "
|
|
@@ -197,15 +198,26 @@ class ClamsApp(ABC):
|
|
|
197
198
|
"no longer be optional in the future. Please pass `runtime_params` "
|
|
198
199
|
"from _annotate() method.",
|
|
199
200
|
FutureWarning, stacklevel=2)
|
|
201
|
+
return
|
|
200
202
|
view.metadata.app = self.metadata.identifier
|
|
201
|
-
if
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
203
|
+
if self._RAW_PARAMS_KEY in runtime_conf:
|
|
204
|
+
for k, v in runtime_conf.items():
|
|
205
|
+
if k == self._RAW_PARAMS_KEY:
|
|
206
|
+
for orik, oriv in v.items():
|
|
207
|
+
if orik in self.metadata.parameters and self.metadata.parameters[orik].multivalued:
|
|
208
|
+
view.metadata.add_parameter(orik, str(oriv))
|
|
209
|
+
else:
|
|
210
|
+
view.metadata.add_parameter(orik, oriv[0])
|
|
211
|
+
view.metadata.add_app_configuration(k, v)
|
|
212
|
+
else:
|
|
213
|
+
# meaning the parameters directly from flask or argparser and values are in lists
|
|
214
|
+
for k, v in runtime_conf.items():
|
|
215
|
+
if k in self.metadata.parameters and self.metadata.parameters[k].multivalued:
|
|
216
|
+
view.metadata.add_parameter(k, str(v))
|
|
217
|
+
else:
|
|
218
|
+
view.metadata.add_parameter(k, v[0])
|
|
207
219
|
|
|
208
|
-
def set_error_view(self, mmif: Union[str, dict, Mmif], runtime_conf:
|
|
220
|
+
def set_error_view(self, mmif: Union[str, dict, Mmif], **runtime_conf: List[str]) -> Mmif:
|
|
209
221
|
"""
|
|
210
222
|
A method to record an error instead of annotation results in the view
|
|
211
223
|
this app generated. For logging purpose, the runtime parameters used
|
|
@@ -282,3 +294,105 @@ class ClamsApp(ABC):
|
|
|
282
294
|
document_file.close()
|
|
283
295
|
else:
|
|
284
296
|
raise FileNotFoundError(p.path)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
class ParameterCaster(object):
|
|
300
|
+
KV_DELIMITER = ':'
|
|
301
|
+
python_type = {"boolean": bool, "number": float, "integer": int, "string": str, "map": dict}
|
|
302
|
+
|
|
303
|
+
"""
|
|
304
|
+
A helper class to convert parameters passed by HTTP query strings to
|
|
305
|
+
proper python data types.
|
|
306
|
+
|
|
307
|
+
:param param_spec: A specification of a data types of parameters
|
|
308
|
+
"""
|
|
309
|
+
def __init__(self, params: Iterable[RuntimeParameter]):
|
|
310
|
+
self.param_spec = {}
|
|
311
|
+
for param in params:
|
|
312
|
+
self.param_spec[param.name] = (self.python_type[param.type], param.multivalued)
|
|
313
|
+
|
|
314
|
+
def cast(self, args: Dict[str, List[str]]) \
|
|
315
|
+
-> Dict[str, Union[real_valued_primitives, List[real_valued_primitives], Dict[str, str]]]:
|
|
316
|
+
"""
|
|
317
|
+
Given parameter specification, tries to cast values of args to specified Python data types.
|
|
318
|
+
Note that this caster deals with query strings, thus all keys and values in the input args are plain strings.
|
|
319
|
+
Also note that the caster does not handle "unexpected" parameters came as an input.
|
|
320
|
+
Handling (raising an exception or issuing a warning upon receiving) an unexpected runtime parameter
|
|
321
|
+
must be done within the app itself.
|
|
322
|
+
Thus, when a key is not found in the parameter specifications, it should just pass it as a vanilla string.
|
|
323
|
+
|
|
324
|
+
:param args: k-v pairs
|
|
325
|
+
:return: A new dictionary of type-casted args, of which keys are always strings (parameter name),
|
|
326
|
+
and values are either
|
|
327
|
+
1) a single value of a specified type (multivalued=False)
|
|
328
|
+
2) a list of values of a specified type (multivalued=True) (all duplicates in the input are kept)
|
|
329
|
+
3) a nested string-string dictionary (type=map ⊨ multivalued=True)
|
|
330
|
+
With the third case, developers can further process the nested values into a more complex data types or
|
|
331
|
+
structures, but that is not in the scope of this Caster class.
|
|
332
|
+
"""
|
|
333
|
+
casted = {}
|
|
334
|
+
for k, vs in args.items():
|
|
335
|
+
assert isinstance(vs, list), f"Expected a list of values for key {k}, but got {vs} of type {type(vs)}"
|
|
336
|
+
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]}"
|
|
337
|
+
if k in self.param_spec:
|
|
338
|
+
for v in vs:
|
|
339
|
+
valuetype, multivalued = self.param_spec[k]
|
|
340
|
+
if multivalued or k not in casted: # effectively only keeps the first value for non-multi params
|
|
341
|
+
if valuetype == bool:
|
|
342
|
+
v = self.bool_param(v)
|
|
343
|
+
elif valuetype == float:
|
|
344
|
+
v = self.float_param(v)
|
|
345
|
+
elif valuetype == int:
|
|
346
|
+
v = self.int_param(v)
|
|
347
|
+
elif valuetype == str:
|
|
348
|
+
v = self.str_param(v)
|
|
349
|
+
elif valuetype == dict:
|
|
350
|
+
v = self.kv_param(v)
|
|
351
|
+
if multivalued:
|
|
352
|
+
if valuetype == dict:
|
|
353
|
+
casted.setdefault(k, {}).update(v)
|
|
354
|
+
else:
|
|
355
|
+
casted.setdefault(k, []).append(v)
|
|
356
|
+
else:
|
|
357
|
+
casted[k] = v
|
|
358
|
+
else:
|
|
359
|
+
if len(vs) > 1:
|
|
360
|
+
casted[k] = vs
|
|
361
|
+
else:
|
|
362
|
+
casted[k] = vs[0]
|
|
363
|
+
return casted # pytype: disable=bad-return-type
|
|
364
|
+
|
|
365
|
+
@staticmethod
|
|
366
|
+
def bool_param(value) -> bool:
|
|
367
|
+
"""
|
|
368
|
+
Helper function to convert string values to bool type.
|
|
369
|
+
"""
|
|
370
|
+
return False if value in (False, 0, 'False', 'false', '0') else True
|
|
371
|
+
|
|
372
|
+
@staticmethod
|
|
373
|
+
def float_param(value) -> float:
|
|
374
|
+
"""
|
|
375
|
+
Helper function to convert string values to float type.
|
|
376
|
+
"""
|
|
377
|
+
return float(value)
|
|
378
|
+
|
|
379
|
+
@staticmethod
|
|
380
|
+
def int_param(value) -> int:
|
|
381
|
+
"""
|
|
382
|
+
Helper function to convert string values to int type.
|
|
383
|
+
"""
|
|
384
|
+
return int(value)
|
|
385
|
+
|
|
386
|
+
@staticmethod
|
|
387
|
+
def str_param(value) -> str:
|
|
388
|
+
"""
|
|
389
|
+
Helper function to convert string values to string type.
|
|
390
|
+
"""
|
|
391
|
+
return value
|
|
392
|
+
|
|
393
|
+
@staticmethod
|
|
394
|
+
def kv_param(value) -> Dict[str, str]:
|
|
395
|
+
"""
|
|
396
|
+
Helper function to convert string values to key-value pair type.
|
|
397
|
+
"""
|
|
398
|
+
return dict([value.split(ParameterCaster.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,7 @@ 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
18
|
|
|
19
19
|
param_value_types_values = param_value_types.__args__ # pytype: disable=attribute-error
|
|
20
20
|
app_directory_baseurl = "http://apps.clams.ai"
|
|
@@ -38,9 +38,12 @@ def generate_app_version(cwd=None):
|
|
|
38
38
|
gitcmd = shutil.which('git')
|
|
39
39
|
gitdir = (Path(sys.modules["__main__"].__file__).parent.resolve() if cwd is None else Path(cwd)) / '.git'
|
|
40
40
|
if gitcmd is not None and gitdir.exists():
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
try:
|
|
42
|
+
proc = subprocess.run([gitcmd, '--git-dir', str(gitdir), 'describe', '--tags', '--always'],
|
|
43
|
+
capture_output=True, check=True)
|
|
44
|
+
return proc.stdout.decode('utf8').strip()
|
|
45
|
+
except subprocess.CalledProcessError:
|
|
46
|
+
return unresolved_app_version_num
|
|
44
47
|
elif app_version_envvar_key in os.environ:
|
|
45
48
|
return os.environ[app_version_envvar_key]
|
|
46
49
|
else:
|
|
@@ -62,16 +65,32 @@ class _BaseModel(pydantic.BaseModel):
|
|
|
62
65
|
|
|
63
66
|
class Output(_BaseModel):
|
|
64
67
|
"""
|
|
65
|
-
|
|
68
|
+
Data model that describes output specification of a CLAMS app.
|
|
69
|
+
|
|
70
|
+
CLAMS apps are expected to have at least one output type, and each type must
|
|
71
|
+
be defined by a ``@type`` URI string. If the type has common properties and values generated by the app,
|
|
72
|
+
they can be described in the (optional) ``properties`` field. Finally, a human-readable
|
|
73
|
+
verbose description can be provided in the (optional) ``description`` field for users.
|
|
74
|
+
|
|
75
|
+
Developers should take diligent care to include all output types and their properties in the app metadata. To
|
|
76
|
+
specify the property values, developers can use an actual value (for full match) or ``'*'`` (for any value).
|
|
66
77
|
"""
|
|
67
78
|
at_type: pydantic.AnyHttpUrl = pydantic.Field(
|
|
68
79
|
...,
|
|
69
80
|
alias="@type",
|
|
70
81
|
description="The type of the object. Must be a IRI string."
|
|
71
82
|
)
|
|
83
|
+
description: Optional[str] = pydantic.Field(
|
|
84
|
+
None,
|
|
85
|
+
description="A verbose, human-readable description of the type. This is intended to be used for documentation "
|
|
86
|
+
"purpose for a particular use case of this annotation type and is not expected to be consumed by "
|
|
87
|
+
"software. This description should work as a guideline for users to understand the output type, "
|
|
88
|
+
"and also can be used as a expansion specification for the type definition beyond the base "
|
|
89
|
+
"vocabulary."
|
|
90
|
+
)
|
|
72
91
|
properties: Dict[str, real_valued_primitives] = pydantic.Field(
|
|
73
92
|
{},
|
|
74
|
-
description="(optional) Specification for type properties, if any."
|
|
93
|
+
description="(optional) Specification for type properties, if any. ``\"*\"`` indicates any value."
|
|
75
94
|
)
|
|
76
95
|
|
|
77
96
|
@pydantic.validator('at_type', pre=True)
|
|
@@ -84,11 +103,24 @@ class Output(_BaseModel):
|
|
|
84
103
|
title = 'CLAMS Output Specification'
|
|
85
104
|
extra = 'forbid'
|
|
86
105
|
allow_population_by_field_name = True
|
|
106
|
+
|
|
107
|
+
def add_description(self, description: str):
|
|
108
|
+
"""
|
|
109
|
+
Add description string to the type specification.
|
|
110
|
+
"""
|
|
111
|
+
self.description = description
|
|
87
112
|
|
|
88
113
|
|
|
89
114
|
class Input(Output):
|
|
90
115
|
"""
|
|
91
|
-
|
|
116
|
+
Data model that describes input specification of a CLAMS app.
|
|
117
|
+
|
|
118
|
+
CLAMS apps are expected to have at least one input type, and each type must
|
|
119
|
+
be defined by a ``@type`` URI string. If the type has specific properties and values required by the app,
|
|
120
|
+
they can be described in the (optional) ``properties`` field. Finally, a human-readable
|
|
121
|
+
verbose description can be provided in the (optional) ``description`` field for users.
|
|
122
|
+
|
|
123
|
+
Developers should take diligent care to include all input types and their properties in the app metadata.
|
|
92
124
|
"""
|
|
93
125
|
required: bool = pydantic.Field(
|
|
94
126
|
None,
|
|
@@ -124,16 +156,33 @@ class RuntimeParameter(_BaseModel):
|
|
|
124
156
|
)
|
|
125
157
|
type: param_value_types = pydantic.Field(
|
|
126
158
|
...,
|
|
127
|
-
description=f"Type of the parameter value the app expects. Must be one of {param_value_types_values}."
|
|
159
|
+
description=f"Type of the parameter value the app expects. Must be one of {param_value_types_values}. When "
|
|
160
|
+
"type is ``map``, ``multivalued=true`` is automatically forced. \n\n"
|
|
161
|
+
"Notes for developers: \n\n"
|
|
162
|
+
"When the type is ``map``, the parameter value (still a single string from the users' perspective) "
|
|
163
|
+
"must be formatted as a ``KEY:VALUE`` pair, namely a colon-separated string. To pass multiple "
|
|
164
|
+
"key-value pairs, users need to pass the parameter multiple times (remember ``type=map`` implies "
|
|
165
|
+
"``multivalued=true``) with pairs in the colon-separated format. \n\n"
|
|
166
|
+
"Also, the `VALUE` part of the user input is always expected and handled as a string. If a "
|
|
167
|
+
"developers wants to do more text processing on the passed value to accept more complex data types "
|
|
168
|
+
"or structures (e.g., map from a string to a list of strings), it is up to the developer. However, "
|
|
169
|
+
"any additional form requirements should be precisely described in the ``description`` field for "
|
|
170
|
+
"users. \n\n"
|
|
171
|
+
"Finally, the same format is expected for the default value, if any. For example, if the default "
|
|
172
|
+
"desired dictionary is ``{'key1': 'value1', 'key2': 'value2'}``, the default value (used when "
|
|
173
|
+
"initializing a parameter) should be ``['key1:value1','key2:value2']``\n."
|
|
128
174
|
)
|
|
129
175
|
choices: List[real_valued_primitives] = pydantic.Field(
|
|
130
176
|
None,
|
|
131
177
|
description="(optional) List of string values that can be accepted."
|
|
132
178
|
)
|
|
133
|
-
default: real_valued_primitives = pydantic.Field(
|
|
179
|
+
default: Union[real_valued_primitives, List[real_valued_primitives]] = pydantic.Field(
|
|
134
180
|
None,
|
|
135
|
-
description="(optional) Default value for the parameter
|
|
136
|
-
"
|
|
181
|
+
description="(optional) Default value for the parameter.\n\n"
|
|
182
|
+
"Notes for developers: \n\n"
|
|
183
|
+
"Setting a default value makes a parameter `optional`. \n\n"
|
|
184
|
+
"When ``multivalued=True``, the default value should be a list of values. \n\n"
|
|
185
|
+
"When ``type=map``, the default value should be a list of colon-separated strings. \n\n"
|
|
137
186
|
)
|
|
138
187
|
multivalued: bool = pydantic.Field(
|
|
139
188
|
...,
|
|
@@ -143,8 +192,15 @@ class RuntimeParameter(_BaseModel):
|
|
|
143
192
|
|
|
144
193
|
def __init__(self, **kwargs):
|
|
145
194
|
super().__init__(**kwargs)
|
|
195
|
+
if self.type == 'map':
|
|
196
|
+
if self.multivalued is False:
|
|
197
|
+
raise ValueError("When the type is `map`, multivalued must not be False.")
|
|
198
|
+
else:
|
|
199
|
+
self.multivalued = True
|
|
146
200
|
if self.multivalued is None:
|
|
147
201
|
self.multivalued = False
|
|
202
|
+
if self.multivalued and self.default is not None and not isinstance(self.default, list):
|
|
203
|
+
self.default = [self.default]
|
|
148
204
|
|
|
149
205
|
class Config:
|
|
150
206
|
title = 'CLAMS App Runtime Parameter'
|
|
@@ -153,7 +209,7 @@ class RuntimeParameter(_BaseModel):
|
|
|
153
209
|
|
|
154
210
|
class AppMetadata(pydantic.BaseModel):
|
|
155
211
|
"""
|
|
156
|
-
|
|
212
|
+
Data model that describes a CLAMS app.
|
|
157
213
|
|
|
158
214
|
Can be initialized by simply passing all required key-value pairs.
|
|
159
215
|
|
|
@@ -287,13 +343,14 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
287
343
|
return True
|
|
288
344
|
return False
|
|
289
345
|
|
|
290
|
-
def add_input(self, at_type: Union[str, vocabulary.ThingTypesBase], required: bool = True, **properties):
|
|
346
|
+
def add_input(self, at_type: Union[str, vocabulary.ThingTypesBase], required: bool = True, **properties) -> Input:
|
|
291
347
|
"""
|
|
292
348
|
Helper method to add an element to the ``input`` list.
|
|
293
349
|
|
|
294
350
|
:param at_type: ``@type`` of the input object
|
|
295
351
|
:param required: whether this type is mandatory or optional
|
|
296
352
|
:param properties: additional property specifications
|
|
353
|
+
:return: the newly added Input object
|
|
297
354
|
"""
|
|
298
355
|
new = Input(at_type=at_type, required=required)
|
|
299
356
|
if len(properties) > 0:
|
|
@@ -306,8 +363,9 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
306
363
|
# TODO (krim @ 5/12/21): automatically add *optional* input types to parameter
|
|
307
364
|
# see https://github.com/clamsproject/clams-python/issues/29 for discussion
|
|
308
365
|
pass
|
|
366
|
+
return new
|
|
309
367
|
|
|
310
|
-
def add_input_oneof(self, *inputs: Union[str, Input, vocabulary.ThingTypesBase]
|
|
368
|
+
def add_input_oneof(self, *inputs: Union[str, Input, vocabulary.ThingTypesBase]):
|
|
311
369
|
newinputs = []
|
|
312
370
|
if len(inputs) == 1:
|
|
313
371
|
if isinstance(inputs[0], Input):
|
|
@@ -328,12 +386,13 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
328
386
|
newinputs.append(i)
|
|
329
387
|
self.input.append(newinputs)
|
|
330
388
|
|
|
331
|
-
def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **properties):
|
|
389
|
+
def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **properties) -> Output:
|
|
332
390
|
"""
|
|
333
391
|
Helper method to add an element to the ``output`` list.
|
|
334
392
|
|
|
335
393
|
:param at_type: ``@type`` of the input object
|
|
336
394
|
:param properties: additional property specifications
|
|
395
|
+
:return: the newly added Output object
|
|
337
396
|
"""
|
|
338
397
|
new = Output(at_type=at_type)
|
|
339
398
|
if len(properties) > 0:
|
|
@@ -342,21 +401,31 @@ class AppMetadata(pydantic.BaseModel):
|
|
|
342
401
|
self.output.append(new)
|
|
343
402
|
else:
|
|
344
403
|
raise ValueError(f"Cannot add a duplicate output '{new}'.")
|
|
404
|
+
return new
|
|
345
405
|
|
|
346
406
|
def add_parameter(self, name: str, description: str, type: param_value_types,
|
|
347
407
|
choices: Optional[List[real_valued_primitives]] = None,
|
|
348
408
|
multivalued: bool = False,
|
|
349
|
-
default: real_valued_primitives = None):
|
|
409
|
+
default: Union[real_valued_primitives, List[real_valued_primitives]] = None):
|
|
350
410
|
"""
|
|
351
411
|
Helper method to add an element to the ``parameters`` list.
|
|
352
412
|
"""
|
|
353
|
-
# casting default values (when the value is not nothing)
|
|
354
|
-
# the values are correctly
|
|
413
|
+
# casting default values (when the value is not nothing) to str
|
|
414
|
+
# will make sure the values are correctly handled by the pydantic
|
|
355
415
|
# see https://docs.pydantic.dev/1.10/usage/types/#unions
|
|
356
416
|
# e.g. casting 0.1 using the `primitives` dict will result in 0 (int)
|
|
357
417
|
# while casting "0.1" using the `primitives` dict will result in 0.1 (float)
|
|
358
|
-
|
|
359
|
-
|
|
418
|
+
if type == 'map' and multivalued is False:
|
|
419
|
+
multivalued = True
|
|
420
|
+
if default is not None:
|
|
421
|
+
if isinstance(default, list):
|
|
422
|
+
default = [str(d) for d in default]
|
|
423
|
+
else:
|
|
424
|
+
default = str(default)
|
|
425
|
+
|
|
426
|
+
new_param = RuntimeParameter(
|
|
427
|
+
name=name, description=description, type=type, choices=choices, multivalued=multivalued,
|
|
428
|
+
default=default)
|
|
360
429
|
if new_param.name not in [param.name for param in self.parameters]:
|
|
361
430
|
self.parameters.append(new_param)
|
|
362
431
|
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.4"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: clams-python
|
|
3
|
-
Version: 1.1.
|
|
3
|
+
Version: 1.1.4
|
|
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.
|
|
17
|
-
from clams.
|
|
16
|
+
from clams.app import ParameterCaster
|
|
17
|
+
from clams.appmetadata import AppMetadata, Input, RuntimeParameter
|
|
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,15 +420,19 @@ class TestRestifier(unittest.TestCase):
|
|
|
374
420
|
class TestParameterCaster(unittest.TestCase):
|
|
375
421
|
|
|
376
422
|
def setUp(self) -> None:
|
|
377
|
-
self.
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
423
|
+
self.params = []
|
|
424
|
+
specs = {'str_param': ('string', False),
|
|
425
|
+
'number_param': ('number', False),
|
|
426
|
+
'int_param': ('integer', False),
|
|
427
|
+
'bool_param': ('boolean', False),
|
|
428
|
+
'str_multi_param': ('string', True),
|
|
429
|
+
'map_param': ('map', True),
|
|
430
|
+
}
|
|
431
|
+
for name, (t, mv) in specs.items():
|
|
432
|
+
self.params.append(RuntimeParameter(**{'name': name, 'type': t, 'multivalued': mv, 'description': ""}))
|
|
433
|
+
|
|
384
434
|
def test_cast(self):
|
|
385
|
-
caster = ParameterCaster(self.
|
|
435
|
+
caster = ParameterCaster(self.params)
|
|
386
436
|
params = {
|
|
387
437
|
'str_param': ["a_string"],
|
|
388
438
|
'number_param': ["1.11"],
|
|
@@ -391,6 +441,7 @@ class TestParameterCaster(unittest.TestCase):
|
|
|
391
441
|
'str_multi_param': ['value1', 'value2'],
|
|
392
442
|
'undefined_param_single': ['undefined_value1'],
|
|
393
443
|
'undefined_param_multi': ['undefined_value1', 'undefined_value2'],
|
|
444
|
+
'map_param': ['key1:val1', 'key2:val2', 'key3:val3', 'key1:val4']
|
|
394
445
|
}
|
|
395
446
|
self.assertTrue(all(map(lambda x: isinstance(x, str), itertools.chain.from_iterable(params.values()))))
|
|
396
447
|
casted = caster.cast(params)
|
|
@@ -404,6 +455,11 @@ class TestParameterCaster(unittest.TestCase):
|
|
|
404
455
|
self.assertTrue('undefined_param_multi' in casted)
|
|
405
456
|
self.assertFalse(isinstance(casted['undefined_param_single'], list))
|
|
406
457
|
self.assertTrue(isinstance(casted['undefined_param_multi'], list))
|
|
458
|
+
self.assertEqual(3, len(casted['map_param']))
|
|
459
|
+
self.assertTrue(all(map(lambda x: isinstance(x, str), casted['map_param'].values())))
|
|
460
|
+
self.assertTrue('val4', len(casted['map_param']['key1']))
|
|
461
|
+
self.assertTrue('val2', len(casted['map_param']['key2']))
|
|
462
|
+
self.assertTrue('val3', len(casted['map_param']['key3']))
|
|
407
463
|
unknown_param_key = 'unknown'
|
|
408
464
|
unknown_param_val = 'dunno'
|
|
409
465
|
params[unknown_param_key] = [unknown_param_val]
|
clams-python-1.1.2/VERSION
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
1.1.2
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "1.1.2"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{clams-python-1.1.2 → clams-python-1.1.4}/clams/develop/templates/app/.dockerignore.template
RENAMED
|
File without changes
|
|
File without changes
|
{clams-python-1.1.2 → clams-python-1.1.4}/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.2 → clams-python-1.1.4}/clams/develop/templates/app/requirements.txt.template
RENAMED
|
File without changes
|
{clams-python-1.1.2 → clams-python-1.1.4}/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.2 → clams-python-1.1.4}/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
|