mmif-python 1.0.17__tar.gz → 1.0.18__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.
Files changed (44) hide show
  1. {mmif_python-1.0.17 → mmif_python-1.0.18}/PKG-INFO +1 -1
  2. mmif_python-1.0.18/VERSION +1 -0
  3. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/serialize/annotation.py +4 -0
  4. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/serialize/mmif.py +49 -14
  5. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/serialize/view.py +11 -0
  6. mmif_python-1.0.18/mmif/ver/__init__.py +2 -0
  7. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_docloc_http/__init__.py +4 -0
  8. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_python.egg-info/PKG-INFO +1 -1
  9. {mmif_python-1.0.17 → mmif_python-1.0.18}/tests/test_serialize.py +33 -11
  10. mmif_python-1.0.17/VERSION +0 -1
  11. mmif_python-1.0.17/mmif/ver/__init__.py +0 -2
  12. {mmif_python-1.0.17 → mmif_python-1.0.18}/LICENSE +0 -0
  13. {mmif_python-1.0.17 → mmif_python-1.0.18}/MANIFEST.in +0 -0
  14. {mmif_python-1.0.17 → mmif_python-1.0.18}/README.md +0 -0
  15. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/__init__.py +0 -0
  16. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/res/__init__.py +0 -0
  17. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/res/clams.vocabulary.yaml +0 -0
  18. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/res/do-not-edit.txt +0 -0
  19. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/res/mmif.json +0 -0
  20. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/serialize/__init__.py +0 -0
  21. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/serialize/model.py +0 -0
  22. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/utils/__init__.py +0 -0
  23. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/utils/sequence_helper.py +0 -0
  24. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/utils/text_document_helper.py +0 -0
  25. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/utils/timeunit_helper.py +0 -0
  26. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/utils/video_document_helper.py +0 -0
  27. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/ver/do-not-edit.txt +0 -0
  28. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/vocabulary/__init__.py +0 -0
  29. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/vocabulary/annotation_types.py +0 -0
  30. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/vocabulary/base_types.py +0 -0
  31. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/vocabulary/do-not-edit.txt +0 -0
  32. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif/vocabulary/document_types.py +0 -0
  33. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_python.egg-info/SOURCES.txt +0 -0
  34. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_python.egg-info/dependency_links.txt +0 -0
  35. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_python.egg-info/requires.txt +0 -0
  36. {mmif_python-1.0.17 → mmif_python-1.0.18}/mmif_python.egg-info/top_level.txt +0 -0
  37. {mmif_python-1.0.17 → mmif_python-1.0.18}/requirements.cv +0 -0
  38. {mmif_python-1.0.17 → mmif_python-1.0.18}/requirements.seq +0 -0
  39. {mmif_python-1.0.17 → mmif_python-1.0.18}/requirements.txt +0 -0
  40. {mmif_python-1.0.17 → mmif_python-1.0.18}/setup.cfg +0 -0
  41. {mmif_python-1.0.17 → mmif_python-1.0.18}/setup.py +0 -0
  42. {mmif_python-1.0.17 → mmif_python-1.0.18}/tests/test_utils.py +0 -0
  43. {mmif_python-1.0.17 → mmif_python-1.0.18}/tests/test_versioncompat.py +0 -0
  44. {mmif_python-1.0.17 → mmif_python-1.0.18}/tests/test_vocab.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.17
3
+ Version: 1.0.18
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  Home-page: https://mmif.clams.ai
6
6
  Author: Brandeis Lab for Linguistics and Computation
@@ -0,0 +1 @@
1
+ 1.0.18
@@ -323,6 +323,10 @@ class Document(Annotation):
323
323
  only keep the latest value (in order of appearances in views list) of
324
324
  the property, effectively overwriting the previous values.
325
325
  """
326
+ # we don't checking if this k-v already exists in _original (new props) or _ephemeral (read from existing MMIF)
327
+ # because it is impossible to keep the _original updated when a new annotation is added (via `new_annotation`)
328
+ # without look across other views and top-level documents list. Also see
329
+ # ``mmif.serialize.mmif.Mmif.generate_capital_annotations`` for where the "de-duplication" happens.
326
330
  if name == "text":
327
331
  self.properties.text = Text(value)
328
332
  elif name == "mime":
@@ -115,13 +115,24 @@ class ViewsList(DataList[View]):
115
115
  """
116
116
  super()._append_with_key(value.id, value, overwrite)
117
117
 
118
- def get_last(self) -> Optional[View]:
118
+ def get_last_contentful_view(self) -> Optional[View]:
119
119
  """
120
- Returns the last view appended to the list.
120
+ Returns the last view that is contentful, i.e., has no error or warning .
121
121
  """
122
122
  for view in reversed(self._items.values()):
123
123
  if 'error' not in view.metadata and 'warning' not in view.metadata:
124
124
  return view
125
+
126
+ def get_last_view(self) -> Optional[View]:
127
+ """
128
+ Returns the last view appended.
129
+ """
130
+ if self._items:
131
+ return self._items[list(self._items.keys())[-1]]
132
+
133
+ def get_last(self) -> Optional[View]:
134
+ warnings.warn('get_last() is deprecated, use get_last_contentful_view() instead.', DeprecationWarning)
135
+ return self.get_last_contentful_view()
125
136
 
126
137
 
127
138
  class Mmif(MmifObject):
@@ -278,13 +289,23 @@ class Mmif(MmifObject):
278
289
  See https://github.com/clamsproject/mmif-python/issues/226 for rationale
279
290
  behind this behavior and discussion.
280
291
  """
281
- # this view will be the default kitchen sink for all generated annotations
282
- last_view = self.views.get_last()
292
+ # this view will be the default kitchen sink for all generated annotations.
293
+ last_view = self.views.get_last_contentful_view()
294
+
283
295
  # proceed only when there's at least one view
284
296
  if last_view:
297
+
298
+ # this app name is used to check a view is generated by the "currently running" app.
299
+ # knowing the currently running app is important so that properties of `Document` objects generated by the
300
+ # current app can be properly recorded inside the `Document` objects (since they are "writable" to the
301
+ # current app), instead of being recorded in a separate `Annotation` object.
302
+ current_app = last_view.metadata.app
303
+
285
304
  # to avoid duplicate property recording, this will be populated with
286
305
  # existing Annotation objects from all existing views
287
306
  existing_anns = defaultdict(lambda: defaultdict(dict))
307
+ # ideally, if we can "de-duplicate" props at `add_property()` time, that'd be more efficient,
308
+ # but that is impossible without looking for the target `document` across other views and top documents list
288
309
 
289
310
  # new properties to record in the current serialization call
290
311
  anns_to_write = defaultdict(dict)
@@ -296,20 +317,30 @@ class Mmif(MmifObject):
296
317
  for ann in view.get_annotations(AnnotationTypes.Annotation):
297
318
  if doc_id is None:
298
319
  doc_id = ann.get_property('document')
320
+ # only if we are sure that the document ID is unique across all views... (with v_id prefix)
321
+ # TODO (krim @ 7/15/24): update id checking once https://github.com/clamsproject/mmif/issues/228 is resolved
322
+ if not any([doc_id == doc.id for doc in self.documents]) and Mmif.id_delimiter not in doc_id:
323
+ doc_id = f"{view.id}{Mmif.id_delimiter}{doc_id}"
299
324
  existing_anns[doc_id].update(ann.properties)
300
325
  for doc in view.get_documents():
301
- anns_to_write[doc.id].update(doc._props_pending)
326
+ anns_to_write[doc.long_id].update(doc._props_pending)
302
327
  for doc in self.documents:
303
- anns_to_write[doc.id].update(doc._props_pending)
328
+ anns_to_write[doc.long_id].update(doc._props_pending)
304
329
  # additional iteration of views, to find a proper view to add the
305
330
  # generated annotations. If none found, use the last view as the kitchen sink
306
331
  last_view_for_docs = defaultdict(lambda: last_view)
307
332
  doc_ids = set(anns_to_write.keys())
308
333
  for doc_id in doc_ids:
334
+ if len(last_view.annotations) == 0:
335
+ # meaning, this new app didn't generate any annotation except for these document properties
336
+ # thus, we should add capital annotations to the last (empty) view
337
+ last_view_for_docs[doc_id] = last_view
338
+ break
309
339
  for view in reversed(self.views):
310
340
  # first try to find out if this view "contains" any annotation to the doc
311
341
  # then, check for individual annotations
312
- if [cont for cont in view.metadata.contains.values() if cont.get('document', None) == doc_id] \
342
+ # TODO (krim @ 7/15/24): update id checking once https://github.com/clamsproject/mmif/issues/228 is resolved
343
+ if [cont for cont in view.metadata.contains.values() if doc_id.endswith(cont.get('document', 'TODO:this endswith test is a temporal solution we use until long_id is forced everywhere'))] \
313
344
  or list(view.get_annotations(document=doc_id)):
314
345
  last_view_for_docs[doc_id] = view
315
346
  break
@@ -323,14 +354,18 @@ class Mmif(MmifObject):
323
354
  if k != 'id' and existing_anns[doc_id][k] != v:
324
355
  props[k] = v
325
356
  if props:
326
- if len(anns_to_write) == 1:
327
- # if there's only one document, we can record the doc_id in the contains metadata
328
- last_view_for_docs[doc_id].metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
329
- props.pop('document', None)
357
+ view_to_write = last_view_for_docs[doc_id]
358
+ if view_to_write.metadata.app == current_app and view_to_write.annotations.get(doc_id) is not None:
359
+ view_to_write.get_document_by_id(doc_id).properties.update(props)
330
360
  else:
331
- # otherwise, doc_id needs to be recorded in the annotation property
332
- props['document'] = doc_id
333
- last_view_for_docs[doc_id].new_annotation(AnnotationTypes.Annotation, **props)
361
+ if len(anns_to_write) == 1:
362
+ # if there's only one document, we can record the doc_id in the contains metadata
363
+ view_to_write.metadata.new_contain(AnnotationTypes.Annotation, document=doc_id)
364
+ props.pop('document', None)
365
+ else:
366
+ # otherwise, doc_id needs to be recorded in the annotation property
367
+ props['document'] = doc_id
368
+ view_to_write.new_annotation(AnnotationTypes.Annotation, **props)
334
369
 
335
370
  def sanitize(self):
336
371
  """
@@ -10,6 +10,7 @@ from datetime import datetime
10
10
  from typing import Dict, Union, Optional, Generator, List, cast
11
11
 
12
12
  from mmif import DocumentTypes, AnnotationTypes, ThingTypesBase, ClamsTypesBase
13
+ from mmif.serialize import model
13
14
  from mmif.serialize.annotation import Annotation, Document
14
15
  from mmif.serialize.model import PRMTV_TYPES, MmifObject, DataList, DataDict
15
16
 
@@ -439,6 +440,16 @@ class AnnotationsList(DataList[Union[Annotation, Document]]):
439
440
  :return: None
440
441
  """
441
442
  super()._append_with_key(value.id, value, overwrite)
443
+
444
+ def __getitem__(self, key: str):
445
+ """
446
+ specialized getter implementation to workaround https://github.com/clamsproject/mmif/issues/228
447
+ # TODO (krim @ 7/12/24): annotation ids must be in the long form in the future, so this check will be unnecessary once https://github.com/clamsproject/mmif/issues/228 is resolved.
448
+ """
449
+ if ":" in key:
450
+ _, aid = key.split(":")
451
+ return self._items.__getitem__(aid)
452
+ return self._items.get(key, None)
442
453
 
443
454
 
444
455
  class ContainsDict(DataDict[ThingTypesBase, Contain]):
@@ -0,0 +1,2 @@
1
+ __version__ = "1.0.18"
2
+ __specver__ = "1.0.5"
@@ -10,3 +10,7 @@ def resolve(docloc):
10
10
  raise ValueError(f'cannot handle document location scheme: {docloc}')
11
11
  except urllib.error.URLError as e:
12
12
  raise e
13
+
14
+
15
+ def help():
16
+ return "location must be a URL string."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: mmif-python
3
- Version: 1.0.17
3
+ Version: 1.0.18
4
4
  Summary: Python implementation of MultiMedia Interchange Format specification. (https://mmif.clams.ai)
5
5
  Home-page: https://mmif.clams.ai
6
6
  Author: Brandeis Lab for Linguistics and Computation
@@ -1118,11 +1118,11 @@ class TestDocument(unittest.TestCase):
1118
1118
  # then converted to an `Annotation` annotation during serialization at `Mmif`-level
1119
1119
  mmif.add_document(doc1)
1120
1120
  ## no Annotation before serialization
1121
- self.assertEqual(0, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation, author='me'))))
1121
+ self.assertEqual(0, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation, author='me'))))
1122
1122
  ## after serialization, the `Annotation` annotation should be added to the last view
1123
1123
  ## note that we didn't add any views, so the last view before and after the serialization are the same
1124
1124
  mmif_roundtrip = Mmif(mmif.serialize())
1125
- self.assertTrue(next(mmif_roundtrip.views.get_last().get_annotations(AnnotationTypes.Annotation, author='me')))
1125
+ self.assertTrue(next(mmif_roundtrip.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation, author='me')))
1126
1126
  # finally, when deserialized back to a Mmif instance, the `Annotation` props should be added
1127
1127
  # as a property of the document
1128
1128
  doc1_mmif_roundtrip = mmif_roundtrip.get_document_by_id('doc1')
@@ -1135,7 +1135,7 @@ class TestDocument(unittest.TestCase):
1135
1135
  mmif.add_document(doc1)
1136
1136
  did = 'doc1'
1137
1137
  # last view before serialization rounds
1138
- r0_vid = mmif.views.get_last().id
1138
+ r0_vid = mmif.views.get_last_contentful_view().id
1139
1139
  doc1.at_type = DocumentTypes.TextDocument
1140
1140
  doc1.id = did
1141
1141
  doc1.location = 'aScheme:///data/doc1.txt'
@@ -1165,7 +1165,7 @@ class TestDocument(unittest.TestCase):
1165
1165
  self.assertEqual(2, len(doc1._props_pending))
1166
1166
  mmif_roundtrip2 = Mmif(mmif_roundtrip1.serialize())
1167
1167
  ## but not serialized
1168
- self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1168
+ self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1169
1169
 
1170
1170
  # adding non-duplicate value should be serialized into a new Annotation object
1171
1171
  # even when there is a duplicate key in a previous view
@@ -1211,7 +1211,7 @@ class TestDocument(unittest.TestCase):
1211
1211
  doc1.add_property('author', 'me')
1212
1212
  mmif_roundtrip = Mmif(mmif.serialize())
1213
1213
  ## should be only one Annotation object, from manual call of `new_annotation`
1214
- self.assertEqual(1, len(list(mmif_roundtrip.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1214
+ self.assertEqual(1, len(list(mmif_roundtrip.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1215
1215
 
1216
1216
  # test with duplicate property added by a downstream app
1217
1217
  mmif_roundtrip = Mmif(mmif.serialize())
@@ -1223,9 +1223,9 @@ class TestDocument(unittest.TestCase):
1223
1223
  # author=me is already in the input MMIF
1224
1224
  doc1_prime.add_property('author', 'me')
1225
1225
  mmif_roundtrip2 = Mmif(mmif_roundtrip.serialize())
1226
- self.assertEqual(1, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1226
+ self.assertEqual(1, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1227
1227
  # none should be added
1228
- self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1228
+ self.assertEqual(0, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1229
1229
 
1230
1230
  # test with same key, different value
1231
1231
  mmif_roundtrip = Mmif(mmif.serialize())
@@ -1238,13 +1238,13 @@ class TestDocument(unittest.TestCase):
1238
1238
  doc1_prime.add_property('author', 'you')
1239
1239
  mmif_roundtrip2 = Mmif(mmif_roundtrip.serialize())
1240
1240
  # the Annotation in the previous view should be preserved
1241
- self.assertEqual(1, len(list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1241
+ self.assertEqual(1, len(list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1242
1242
  # but a new one should be added
1243
- self.assertEqual(1, len(list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))))
1243
+ self.assertEqual(1, len(list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))))
1244
1244
 
1245
- self.assertEqual('me', list(mmif.views.get_last().get_annotations(AnnotationTypes.Annotation))[0].get_property(
1245
+ self.assertEqual('me', list(mmif.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[0].get_property(
1246
1246
  'author'))
1247
- self.assertEqual('you', list(mmif_roundtrip2.views.get_last().get_annotations(AnnotationTypes.Annotation))[
1247
+ self.assertEqual('you', list(mmif_roundtrip2.views.get_last_contentful_view().get_annotations(AnnotationTypes.Annotation))[
1248
1248
  0].get_property('author'))
1249
1249
 
1250
1250
  def test_capital_annotation_generation_viewfinder(self):
@@ -1269,6 +1269,28 @@ class TestDocument(unittest.TestCase):
1269
1269
  cap_anns = list(mmif_roundtrip.views[f'v{i}'].get_annotations(AnnotationTypes.Annotation))
1270
1270
  self.assertEqual(1, len(cap_anns))
1271
1271
  self.assertEqual(authors[i-1], cap_anns[0].get_property('author'))
1272
+
1273
+ def test_capital_annotation_nongeneration_for_writable_documents(self):
1274
+ mmif = Mmif(validate=False)
1275
+ doc = Document()
1276
+ doc.at_type = DocumentTypes.TextDocument
1277
+ doc.id = f'doc0'
1278
+ doc.location = f'aScheme:///data/doc0.txt'
1279
+ mmif.add_document(doc)
1280
+
1281
+ v = mmif.new_view()
1282
+ v.metadata.app = tester_appname
1283
+ vid = v.id
1284
+ new_td_id = mmif[vid].new_textdocument(text='new text', document='doc0', origin='transformation').id
1285
+ doc.add_property('author', 'me')
1286
+
1287
+ mmif_roundtrip = Mmif(mmif.serialize())
1288
+
1289
+ self.assertTrue(AnnotationTypes.Annotation in mmif_roundtrip[vid].metadata.contains)
1290
+ self.assertTrue(mmif_roundtrip.get_document_by_id('doc0').get_property('author'), 'me')
1291
+ self.assertTrue(next(mmif_roundtrip[vid].get_annotations(AnnotationTypes.Annotation)).get_property('author'), 'me')
1292
+ self.assertTrue(next(mmif_roundtrip[vid].get_annotations(AnnotationTypes.Annotation)).get_property('document'), doc.id)
1293
+ self.assertTrue(mmif_roundtrip[new_td_id].get_property('origin'), 'transformation')
1272
1294
 
1273
1295
  def test_deserialize_with_whole_mmif(self):
1274
1296
  for i, datum in self.data.items():
@@ -1 +0,0 @@
1
- 1.0.17
@@ -1,2 +0,0 @@
1
- __version__ = "1.0.17"
2
- __specver__ = "1.0.5"
File without changes
File without changes
File without changes
File without changes
File without changes