Skip to content

Commit

Permalink
Merge pull request #193 from clamsproject/develop
Browse files Browse the repository at this point in the history
releasing 1.1.1
  • Loading branch information
keighrim authored Feb 5, 2024
2 parents 54806f4 + 362ae37 commit 3a9bfc8
Show file tree
Hide file tree
Showing 7 changed files with 40 additions and 29 deletions.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ docs: VERSION $(generatedcode)
rm -rf docs
mkdir -p docs
python3 clams/appmetadata/__init__.py > documentation/appmetadata.jsonschema
sphinx-build -b html documentation/ docs
sphinx-build -a -b html documentation/ docs
mv documentation/appmetadata.jsonschema docs/
touch docs/.nojekyll
echo 'sdk.clams.ai' > docs/CNAME
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,10 @@
1. a flask-based wrapper to run a Python CLAMS app as an HTTP web app
1. cookie-cutter shell command that sets up everything and starts a new CLAMS app project

### Start now from [Getting Started](introduction.html)!
### Start now from [Getting Started](https://clamsproject.github.io/clams-python/introduction.html)!

## For more ...
For user manuals and Python API documentation, take a look at [Python modules](https://clams.ai/clams-python/modules.html).

For MMIF-specific Python API documentation, take a look at the [mmif-python website](https://clams.ai/mmif-python).
* [Version history and patch notes](https://github.com/clamsproject/clams-python/blob/main/CHANGELOG.md)
* [CLAMS Python API documentation](https://clamsproject.github.io/clams-python/modules.html)
* [MMIF-specific Python API documentation](https://clamsproject.github.io/mmif-python)
* [Public CLAMS App Directory](https://clamsproject.github.io/apps)
3 changes: 1 addition & 2 deletions clams/app/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,6 @@ def _annotate(self, mmif: Mmif, _raw_parameters=None, **refined_parameters) -> M
#. Create a new view (or views) by calling :meth:`~mmif.serialize.mmif.Mmif.new_view` on the input mmif object.
#. Call :meth:`~clams.app.ClamsApp.sign_view` with the input runtime parameters for the record.
#. Call :meth:`~clams.app.ClamsApp.get_configuration` to get an "upgraded" runtime parameters with default values.
#. Call :meth:`~mmif.serialize.view.View.new_contain` on the new view object with any annotation properties specified by the configuration.
#. Process the data and create :class:`~mmif.serialize.annotation.Annotation` objects and add them to the new view.
#. While doing so, get help from :class:`~mmif.vocabulary.document_types.DocumentTypes`, :class:`~mmif.vocabulary.annotation_types.AnnotationTypes` classes to generate ``@type`` strings.
Expand Down Expand Up @@ -177,7 +176,7 @@ def _refine_params(self, **runtime_params):
def get_configuration(self, **runtime_params):
warnings.warn("ClamsApp.get_configuration() is deprecated. "
"If you are using this method in `_annotate()` method,"
"it is no longer needed since `clams-python==1.0.10`.",
"it is no longer needed since `clams-python>1.0.9`.",
DeprecationWarning, stacklevel=2)
return self._refine_params(**runtime_params)

Expand Down
12 changes: 6 additions & 6 deletions clams/appmetadata/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
unresolved_app_version_num = 'unresolvable'
app_version_envvar_key = 'CLAMS_APP_VERSION'
# type aliases to use in app metadata and runtime parameter processing
primitives = Union[int, float, bool, str]
real_valued_primitives = Union[int, float, bool, str]
# these names are taken from the JSON schema data types
param_value_types = Literal['integer', 'number', 'string', 'boolean']

Expand Down Expand Up @@ -69,7 +69,7 @@ class Output(_BaseModel):
alias="@type",
description="The type of the object. Must be a IRI string."
)
properties: Dict[str, str] = pydantic.Field(
properties: Dict[str, real_valued_primitives] = pydantic.Field(
{},
description="(optional) Specification for type properties, if any."
)
Expand Down Expand Up @@ -126,11 +126,11 @@ class RuntimeParameter(_BaseModel):
...,
description=f"Type of the parameter value the app expects. Must be one of {param_value_types_values}."
)
choices: List[primitives] = pydantic.Field(
choices: List[real_valued_primitives] = pydantic.Field(
None,
description="(optional) List of string values that can be accepted."
)
default: primitives = pydantic.Field(
default: real_valued_primitives = pydantic.Field(
None,
description="(optional) Default value for the parameter. Only valid for optional parameters. Namely, setting "
"a default value makes a parameter 'optional'."
Expand Down Expand Up @@ -344,9 +344,9 @@ def add_output(self, at_type: Union[str, vocabulary.ThingTypesBase], **propertie
raise ValueError(f"Cannot add a duplicate output '{new}'.")

def add_parameter(self, name: str, description: str, type: param_value_types,
choices: Optional[List[primitives]] = None,
choices: Optional[List[real_valued_primitives]] = None,
multivalued: bool = False,
default: primitives = None):
default: real_valued_primitives = None):
"""
Helper method to add an element to the ``parameters`` list.
"""
Expand Down
11 changes: 7 additions & 4 deletions clams/restify/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from mmif import Mmif

from clams.app import ClamsApp
from clams.appmetadata import primitives
from clams.appmetadata import real_valued_primitives


class Restifier(object):
Expand Down Expand Up @@ -160,10 +160,10 @@ class ParameterCaster(object):
:param param_spec: A specification of a data types of parameters
"""
def __init__(self, param_spec: Dict[str, Tuple[primitives, bool]]):
def __init__(self, param_spec: Dict[str, Tuple[real_valued_primitives, bool]]):
self.param_spec = param_spec

def cast(self, args: Dict[str, List[str]]) -> Dict[str, Union[primitives, List[primitives]]]:
def cast(self, args: Dict[str, List[str]]) -> Dict[str, Union[real_valued_primitives, List[real_valued_primitives]]]:
"""
Given parameter specification, tries to cast values of args to specified Python data types.
Note that this caster deals with query strings, thus all keys and values in the input args are plain strings.
Expand Down Expand Up @@ -194,7 +194,10 @@ def cast(self, args: Dict[str, List[str]]) -> Dict[str, Union[primitives, List[p
else:
casted[k] = v
else:
casted[k] = vs
if len(vs) > 1:
casted[k] = vs
else:
casted[k] = vs[0]

return casted

Expand Down
14 changes: 8 additions & 6 deletions documentation/tutorial.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,13 +113,15 @@ def _appmetadata(self):
> Also refer to [CLAMS App Metadata](https://sdk.clams.ai/appmetadata.html) for more details regarding what fields need to be specified.
#### `_annotate()`
The `_annotate()` method should accept a MMIF file as its parameter and always returns a `MMIF` object with an additional `view` containing annotation results. This is where the bulk of your logic will go. For a text processing app, it is mostly concerned with finding text documents, calling the code that runs over the text, creating new views and inserting the results.
The `_annotate()` method should accept a MMIF file/string/object as its first parameter and always returns a `MMIF` object with an additional `view` containing annotation results. This is where the bulk of your logic will go. For a text processing app, it is mostly concerned with finding text documents, calling the code that runs over the text, creating new views and inserting the results.
In addition to the input MMIF, this method can accept any number of keyword arguments, which are the parameters set by the user/caller. Note that when this method is called inside the `annotate()` public method in the `ClamsApp` class (which is the usual case when running as a CLAMS app), the keyword arguments are automatically "refined" before being passed here. The refinement includes
1. inserting "default" values for parameters that are not set by the user
2. checking that the values are of the correct type and value, based on the parameter specification in the app metadata
```python
def _annotate(self, mmif, **kwargs):
# before everything, this will populate the argument dict with the default values
# and while doing so, it will also do some validation of the argument values
configs = self.get_configuration(kwargs)
# then, access the parameters: here to just print
# them and to willy-nilly throw an error if the caller wants that
for arg, val in configs.items():
Expand All @@ -146,7 +148,7 @@ The method `_run_nlp_tool()` is responsible for running the NLP tool and adding

One thing about `_annotate()` as it is defined above is that it will most likely be the same for each NLP application, all the application specific details are in the code that creates new views and the code that adds annotations.

Creating a new view:
##### Creating a new view:

```python
def _new_view(self, docid=None, runtime_config):
Expand All @@ -163,7 +165,7 @@ def _new_view(self, docid=None, runtime_config):

This is the simplest NLP view possible since there is only one annotation type and it has no metadata properties beyond the `document` property. Other applications may have more annotation types, which results in repeated invocations of `new_contain()`, and may define other metadata properties for those types.

Adding annotations:
##### Adding annotations:

```python
def _run_nlp_tool(self, doc, new_view, full_doc_id):
Expand Down
18 changes: 12 additions & 6 deletions tests/test_clamsapp.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ def test_appmetadata(self):
self.app.metadata.add_output(AnnotationTypes.BoundingBox, boxType='text')
metadata = json.loads(self.app.appmetadata())
self.assertEqual(len(metadata['output']), 2)
# these should not be added as a duplicate
# these should not be added because they are duplicates
with self.assertRaises(ValueError):
self.app.metadata.add_input(at_type=DocumentTypes.TextDocument)
with self.assertRaises(ValueError):
Expand Down Expand Up @@ -240,14 +240,14 @@ def test_open_document_location_custom_opener(self):
with self.app.open_document_location(mmif.documents['i1'], Image.open) as f:
self.assertEqual(f.size, (200, 71))

def test_get_configuration(self):
def test_refine_parameters(self):
self.app.metadata.parameters = []
self.app.metadata.add_parameter('param1', 'first_param', 'string')
self.app.metadata.add_parameter('param2', 'second_param', 'string', default='second_default')
self.app.metadata.add_parameter('param3', 'third_param', 'boolean', default='f')
self.app.metadata.add_parameter('param4', 'fourth_param', 'integer', default='1', choices="1 2 3".split())
self.app.metadata.add_parameter('param5', 'fifth_param', 'number', default='0.5')
conf = self.app.get_configuration(param1='okay', non_parameter='should be ignored')
conf = self.app._refine_params(param1='okay', non_parameter='should be ignored')
conf.pop(clams.ClamsApp._RAW_PARAMS_KEY, None)
self.assertEqual(len(conf), 5) # 1 from `param1`, 4 from default value
self.assertFalse('non_parameter' in conf)
Expand All @@ -258,10 +258,10 @@ def test_get_configuration(self):
self.assertEqual(type(conf['param5']), float)
with self.assertRaises(ValueError):
# because param1 doesn't have a default value and thus a required param
self.app.get_configuration(param2='okay')
self.app._refine_params(param2='okay')
with self.assertRaisesRegexp(ValueError, r'.+must be one of.+'):
# because param4 can't be 4, note that param1 is "required"
self.app.get_configuration(param1='p1', param4=4)
self.app._refine_params(param1='p1', param4=4)

def test_error_handling(self):
params = {'raise_error': True, 'pretty': True}
Expand Down Expand Up @@ -383,7 +383,9 @@ def test_cast(self):
'number_param': ["1.11"],
'int_param': [str(sys.maxsize)],
'bool_param': ['true'],
'str_multi_param': ['value1', 'value2']
'str_multi_param': ['value1', 'value2'],
'undefined_param_single': ['undefined_value1'],
'undefined_param_multi': ['undefined_value1', 'undefined_value2'],
}
self.assertTrue(all(map(lambda x: isinstance(x, str), itertools.chain.from_iterable(params.values()))))
casted = caster.cast(params)
Expand All @@ -393,6 +395,10 @@ def test_cast(self):
self.assertTrue(isinstance(casted['int_param'], int))
self.assertTrue(casted['bool_param'])
self.assertEqual(set(casted['str_multi_param']), set(params['str_multi_param']))
self.assertTrue('undefined_param_single' in casted)
self.assertTrue('undefined_param_multi' in casted)
self.assertFalse(isinstance(casted['undefined_param_single'], list))
self.assertTrue(isinstance(casted['undefined_param_multi'], list))
unknown_param_key = 'unknown'
unknown_param_val = 'dunno'
params[unknown_param_key] = [unknown_param_val]
Expand Down

0 comments on commit 3a9bfc8

Please sign in to comment.