diff --git a/src/annotation.rs b/src/annotation.rs index d2836ff..33052ed 100644 --- a/src/annotation.rs +++ b/src/annotation.rs @@ -10,7 +10,7 @@ use crate::annotationdata::{PyAnnotationData, PyData}; use crate::annotationdataset::PyAnnotationDataSet; use crate::annotationstore::MapStore; use crate::error::PyStamError; -use crate::iterparams::IterParams; +use crate::query::*; use crate::resources::{PyOffset, PyTextResource}; use crate::selector::{PySelector, PySelectorKind}; use crate::textselection::{PyTextSelectionOperator, PyTextSelections}; @@ -92,6 +92,8 @@ impl PyAnnotation { annotation: Some(annotation.handle()), resource: None, dataset: None, + key: None, + data: None, offset: if annotation .as_ref() .target() @@ -128,9 +130,9 @@ impl PyAnnotation { /// Returns the text of the annotation. /// If the annotation references multiple text slices, they will be concatenated with a space as a delimiter, - /// but note that in reality the different parts may be non-contingent! + /// but note that in reality the different parts may be non-contingent or non-delimited! /// - /// Use `text()` instead to retrieve a list of texts + /// Use `text()` instead to retrieve a list of texts, which you can subsequently concatenate as you please. fn __str__(&self) -> PyResult { self.map(|annotation| { let elements: Vec<&str> = annotation.text().collect(); @@ -142,52 +144,146 @@ impl PyAnnotation { /// Returns a list of all textselections of the annotation. /// Note that this will always return a list (even it if only contains a single element), /// as an annotation may reference multiple text selections. - #[pyo3(signature = (**kwargs))] - fn textselections(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.textselections(); - iterparams.evaluate_to_pytextselections(iter, annotation.store(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn textselections( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotation| { + Ok(PyTextSelections::from_iter( + annotation.textselections().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |annotation, query| { + Ok(PyTextSelections::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } /// Returns annotations this annotation refers to (i.e. using an AnnotationSelector) - #[pyo3(signature = (**kwargs))] - fn annotations_in_targets(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - let mut recursive: bool = false; - if let Some(kwargs) = kwargs { - if let Ok(Some(v)) = kwargs.get_item("recursive") { - if let Ok(v) = v.extract() { - recursive = v; - } - } + #[pyo3(signature = (*args, **kwargs))] + fn annotations_in_targets( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let limit = get_limit(kwargs); + let recursive = get_recursive(kwargs, AnnotationDepth::One); + if !has_filters(args, kwargs) { + self.map(|annotation| { + Ok(PyAnnotations::from_iter( + annotation.annotations_in_targets(recursive).limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable("main", SelectionQualifier::Metadata, recursive), + args, + kwargs, + |annotation, query| { + Ok(PyAnnotations::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) } - self.map(|annotation| { - let iter = annotation.annotations_in_targets(recursive); - iterparams.evaluate_to_pyannotations(iter, annotation.store(), &self.store) - }) } /// Returns annotations that are referring to this annotation (i.e. others using an AnnotationSelector) - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.annotations(); - iterparams.evaluate_to_pyannotations(iter, annotation.store(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotation| { + Ok(PyAnnotations::from_iter( + annotation.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |annotation, query| { + Ok(PyAnnotations::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.annotations(); - Ok(iterparams - .evaluate_annotations(iter, annotation.store())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|annotation| Ok(annotation.annotations().test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |annotation, query| Ok(annotation.store().query(query).test()), + ) + } + } + + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations_in_targets( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let recursive = get_recursive(kwargs, AnnotationDepth::One); + if !has_filters(args, kwargs) { + self.map(|annotation| Ok(annotation.annotations_in_targets(recursive).test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable("main", SelectionQualifier::Metadata, recursive), + args, + kwargs, + |annotation, query| Ok(annotation.store().query(query).test()), + ) + } } /// Returns a list of resources this annotation refers to @@ -261,22 +357,55 @@ impl PyAnnotation { } /// Returns annotation data instances that pertain to this annotation. - #[pyo3(signature = (**kwargs))] - fn data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.data(); - iterparams.evaluate_to_pydata(iter, annotation.store(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotation| { + Ok(PyData::from_iter( + annotation.data().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |annotation, query| { + Ok(PyData::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.data(); - Ok(iterparams.evaluate_data(iter, annotation.store())?.test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|annotation| Ok(annotation.data().test())) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |annotation, query| Ok(annotation.store().query(query).test()), + ) + } } /// Returns the number of data items under this annotation @@ -285,17 +414,40 @@ impl PyAnnotation { .unwrap() } - #[pyo3(signature = (operator, **kwargs))] + #[pyo3(signature = (operator, *args, **kwargs))] fn related_text( &self, operator: PyTextSelectionOperator, + args: &PyTuple, kwargs: Option<&PyDict>, ) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotation| { - let iter = annotation.related_text(operator.operator); - iterparams.evaluate_to_pytextselections(iter, annotation.rootstore(), &self.store) - }) + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotation| { + Ok(PyTextSelections::from_iter( + annotation.related_text(operator.operator).limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::TextRelation { + var: "main", + operator: operator.operator, //MAYBE TODO: check if we need to invert an operator here? + }, + args, + kwargs, + |annotation, query| { + Ok(PyTextSelections::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } } @@ -304,8 +456,6 @@ pub struct PyAnnotations { pub(crate) annotations: Vec, pub(crate) store: Arc>, pub(crate) cursor: usize, - /// Indicates whether the annotations are sorted chronologically (by handle) - pub(crate) sorted: bool, } #[pymethods] @@ -344,87 +494,219 @@ impl PyAnnotations { !pyself.annotations.is_empty() } - fn is_sorted(pyself: PyRef<'_, Self>) -> bool { - pyself.sorted + /// Returns annotation data instances used by the annotations in this collection. + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotations, store| { + Ok(PyData::from_iter( + annotations.items().data().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |query, store| Ok(PyData::from_query(query, store, &self.store, limit)), + ) + } } - /// Returns annotation data instances used by the annotations in this collection. - #[pyo3(signature = (**kwargs))] - fn data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .data(); - iterparams.evaluate_to_pydata(iter, store, &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|annotations, _| Ok(annotations.items().data().test())) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |query, store| Ok(store.query(query).test()), + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .data(); - Ok(iterparams.evaluate_data(iter, store)?.test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotations, store| { + Ok(PyAnnotations::from_iter( + annotations.items().annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |query, store| Ok(PyAnnotations::from_query(query, store, &self.store, limit)), + ) + } } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .annotations(); - iterparams.evaluate_to_pyannotations(iter, store, &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|annotations, _| Ok(annotations.items().annotations().test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |query, store| Ok(store.query(query).test()), + ) + } } - #[pyo3(signature = (**kwargs))] - fn annotations_in_targets(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - let mut recursive: bool = false; - if let Some(kwargs) = kwargs { - if let Ok(Some(v)) = kwargs.get_item("recursive") { - if let Ok(v) = v.extract() { - recursive = v; - } - } + #[pyo3(signature = (*args, **kwargs))] + fn annotations_in_targets( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let limit = get_limit(kwargs); + let recursive = get_recursive(kwargs, AnnotationDepth::One); + if !has_filters(args, kwargs) { + self.map(|annotations, store| { + Ok(PyAnnotations::from_iter( + annotations + .items() + .annotations_in_targets(recursive) + .limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable("main", SelectionQualifier::Normal, recursive), + args, + kwargs, + |query, store| Ok(PyAnnotations::from_query(query, store, &self.store, limit)), + ) } - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .annotations_in_targets(recursive); - iterparams.evaluate_to_pyannotations(iter, store, &self.store) - }) } - #[pyo3(signature = (**kwargs))] - fn textselections(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .textselections(); - iterparams.evaluate_to_pytextselections(iter, store, &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations_in_targets( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let recursive = get_recursive(kwargs, AnnotationDepth::One); + if !has_filters(args, kwargs) { + self.map(|annotations, _| { + Ok(annotations.items().annotations_in_targets(recursive).test()) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::AnnotationVariable("main", SelectionQualifier::Normal, recursive), + args, + kwargs, + |query, store| Ok(store.query(query).test()), + ) + } } - #[pyo3(signature = (operator, **kwargs))] + #[pyo3(signature = (*args,**kwargs))] + fn textselections( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotations, store| { + Ok(PyTextSelections::from_iter( + annotations.items().textselections().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::AnnotationVariable( + "main", + SelectionQualifier::Normal, + AnnotationDepth::One, + ), + args, + kwargs, + |query, store| { + Ok(PyTextSelections::from_query( + query, + store, + &self.store, + limit, + )) + }, + ) + } + } + + #[pyo3(signature = (operator, *args, **kwargs))] fn related_text( &self, operator: PyTextSelectionOperator, + args: &PyTuple, kwargs: Option<&PyDict>, ) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|annotations, store| { - let iter = Annotations::from_handles(Cow::Borrowed(annotations), self.sorted, store) - .iter() - .related_text(operator.operator); - iterparams.evaluate_to_pytextselections(iter, store, &self.store) - }) + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|annotations, store| { + Ok(PyTextSelections::from_iter( + annotations + .items() + .related_text(operator.operator) + .limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::TextRelation { + var: "main", + operator: operator.operator, //MAYBE TODO: check if we need to invert an operator here? + }, + args, + kwargs, + |query, store| { + Ok(PyTextSelections::from_query( + query, + store, + &self.store, + limit, + )) + }, + ) + } } fn textual_order(mut pyself: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { @@ -447,12 +729,49 @@ impl PyAnnotations { } impl PyAnnotations { + pub(crate) fn from_iter<'store>( + iter: impl Iterator>, + wrappedstore: &Arc>, + ) -> Self { + Self { + annotations: iter.map(|x| x.handle()).collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + + pub(crate) fn from_query<'store>( + query: Query<'store>, + store: &'store AnnotationStore, + wrappedstore: &Arc>, + limit: Option, + ) -> Self { + assert!(query.resulttype() == Some(Type::Annotation)); + Self { + annotations: store + .query(query) + .limit(limit) + .map(|mut resultitems| { + //we use the deepest item if there are multiple + if let Some(QueryResultItem::Annotation(annotation)) = resultitems.pop_last() { + annotation.handle() + } else { + unreachable!("Unexpected QueryResultItem"); + } + }) + .collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + fn map(&self, f: F) -> Result where - F: FnOnce(&Vec, &AnnotationStore) -> Result, + F: FnOnce(Handles, &AnnotationStore) -> Result, { if let Ok(store) = self.store.read() { - f(&self.annotations, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) + let handles = Annotations::new(Cow::Borrowed(&self.annotations), false, &store); + f(handles, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) } else { Err(PyRuntimeError::new_err( "Unable to obtain store (should never happen)", @@ -472,6 +791,40 @@ impl PyAnnotations { )) } } + + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(Query, &AnnotationStore) -> Result, + { + self.map(|annotations, store| { + let query = Query::new(QueryType::Select, Some(Type::Annotation), Some("main")) + .with_constraint(Constraint::Annotations( + annotations, + SelectionQualifier::Normal, + AnnotationDepth::One, + )) + .with_subquery( + build_query( + Query::new(QueryType::Select, Some(resulttype), Some("sub")) + .with_constraint(constraint), + args, + kwargs, + store, + ) + .map_err(|e| { + StamError::QuerySyntaxError(format!("{}", e), "(python to query)") + })?, + ); + f(query, store) + }) + } } #[pyclass(name = "DataIter")] @@ -533,7 +886,7 @@ impl MapStore for PyAnnotation { impl PyAnnotation { /// Map function to act on the actual underlying store, helps reduce boilerplate - fn map(&self, f: F) -> Result + pub(crate) fn map(&self, f: F) -> Result where F: FnOnce(ResultItem<'_, Annotation>) -> Result, { @@ -548,34 +901,29 @@ impl PyAnnotation { )) } } -} -impl<'py> IterParams<'py> { - pub(crate) fn evaluate_to_pyannotations<'store>( - self, - iter: AnnotationsIter<'store>, - store: &'store AnnotationStore, - wrappedstore: &Arc>, - ) -> Result + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result where - 'py: 'store, + F: FnOnce(ResultItem, Query) -> Result, { - let limit = self.limit(); - match self.evaluate_annotations(iter, store) { - Ok(iter) => { - let sorted = iter.returns_sorted(); - Ok(PyAnnotations { - annotations: if let Some(limit) = limit { - iter.to_collection_limit(limit).take() - } else { - iter.to_collection().take() - }, - store: wrappedstore.clone(), - cursor: 0, - sorted, - }) - } - Err(e) => Err(e.into()), - } + self.map(|annotation| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(constraint), + args, + kwargs, + annotation.store(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_annotationvar("main", annotation.clone()); + f(annotation, query) + }) } } diff --git a/src/annotationdata.rs b/src/annotationdata.rs index 3da5173..b1c253c 100644 --- a/src/annotationdata.rs +++ b/src/annotationdata.rs @@ -11,7 +11,8 @@ use crate::annotation::PyAnnotations; use crate::annotationdataset::PyAnnotationDataSet; use crate::annotationstore::MapStore; use crate::error::PyStamError; -use crate::iterparams::IterParams; +use crate::query::*; +use crate::selector::{PySelector, PySelectorKind}; use stam::*; #[pyclass(dict, module = "stam", name = "DataKey")] @@ -81,47 +82,88 @@ impl PyDataKey { }) } - #[pyo3(signature = (**kwargs))] - fn data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|key| { - let iter = key.data(); - iterparams.evaluate_to_pydata(iter, key.rootstore(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|key| Ok(PyData::from_iter(key.data().limit(limit), &self.store))) + } else { + self.map_with_query(Type::AnnotationData, args, kwargs, |key, query| { + Ok(PyData::from_query( + query, + key.rootstore(), + &self.store, + limit, + )) + }) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|key| { - let iter = key.data(); - Ok(iterparams.evaluate_data(iter, key.rootstore())?.test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|key| Ok(key.data().test())) + } else { + self.map_with_query(Type::AnnotationData, args, kwargs, |key, query| { + Ok(key.rootstore().query(query).test()) + }) + } } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|key| { - let iter = key.annotations(); - iterparams.evaluate_to_pyannotations(iter, key.rootstore(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|key| { + Ok(PyAnnotations::from_iter( + key.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |key, query| { + Ok(PyAnnotations::from_query( + query, + key.rootstore(), + &self.store, + limit, + )) + }) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|key| { - let iter = key.annotations(); - Ok(iterparams - .evaluate_annotations(iter, key.rootstore())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|key| Ok(key.annotations().test())) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |key, query| { + Ok(key.rootstore().query(query).test()) + }) + } } fn annotations_count(&self) -> usize { self.map(|key| Ok(key.annotations_count())).unwrap() } + + /// Returns a Selector (DataKeySelector) pointing to this DataKey + fn select(&self) -> PyResult { + self.map(|key| { + Ok(PySelector { + kind: PySelectorKind { + kind: SelectorKind::DataKeySelector, + }, + dataset: None, + annotation: None, + resource: None, + key: Some((key.set().handle(), key.handle())), + data: None, + offset: None, + subselectors: Vec::new(), + }) + }) + } } impl MapStore for PyDataKey { @@ -134,7 +176,7 @@ impl MapStore for PyDataKey { } impl PyDataKey { - fn map(&self, f: F) -> Result + pub(crate) fn map(&self, f: F) -> Result where F: FnOnce(ResultItem) -> Result, { @@ -152,6 +194,30 @@ impl PyDataKey { )) } } + + fn map_with_query( + &self, + resulttype: Type, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(ResultItem, Query) -> Result, + { + self.map(|key| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(Constraint::KeyVariable("main", SelectionQualifier::Normal)), + args, + kwargs, + key.rootstore(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_keyvar("main", key.clone()); + f(key, query) + }) + } } #[pyclass(dict, module = "stam", name = "AnnotationData")] @@ -396,29 +462,60 @@ impl PyAnnotationData { Ok(PyAnnotationDataSet::new(self.set, &self.store)) } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|data| { - let iter = data.annotations(); - iterparams.evaluate_to_pyannotations(iter, data.rootstore(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|data| { + Ok(PyAnnotations::from_iter( + data.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |data, query| { + Ok(PyAnnotations::from_query( + query, + data.rootstore(), + &self.store, + limit, + )) + }) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|data| { - let iter = data.annotations(); - Ok(iterparams - .evaluate_annotations(iter, data.rootstore())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|key| Ok(key.annotations().test())) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |key, query| { + Ok(key.rootstore().query(query).test()) + }) + } } fn annotations_len(&self) -> usize { self.map(|data| Ok(data.annotations_len())).unwrap() } + + /// Returns a Selector (AnnotationDataSelector) pointing to this AnnotationData + fn select(&self) -> PyResult { + self.map(|data| { + Ok(PySelector { + kind: PySelectorKind { + kind: SelectorKind::AnnotationDataSelector, + }, + dataset: None, + annotation: None, + resource: None, + data: Some((data.set().handle(), data.handle())), + key: None, + offset: None, + subselectors: Vec::new(), + }) + }) + } } impl MapStore for PyAnnotationData { @@ -431,7 +528,7 @@ impl MapStore for PyAnnotationData { } impl PyAnnotationData { - fn map(&self, f: F) -> Result + pub(crate) fn map(&self, f: F) -> Result where F: FnOnce(ResultItem) -> Result, { @@ -449,6 +546,30 @@ impl PyAnnotationData { )) } } + + fn map_with_query( + &self, + resulttype: Type, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(ResultItem, Query) -> Result, + { + self.map(|data| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(Constraint::DataVariable("main", SelectionQualifier::Normal)), + args, + kwargs, + data.rootstore(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_datavar("main", data.clone()); + f(data, query) + }) + } } /// Build an AnnotationDataBuilder from a python dictionary (or string referring to an existing public ID) @@ -508,12 +629,7 @@ pub(crate) fn annotationdata_builder<'a>(data: &'a PyAny) -> PyResult( - kwargs: &'py PyDict, -) -> Result>, StamError> -where - 'py: 'a, -{ +pub(crate) fn dataoperator_from_kwargs(kwargs: &PyDict) -> Result, StamError> { if let Ok(Some(value)) = kwargs.get_item("value") { Ok(Some(dataoperator_from_py(value)?)) } else if let Ok(Some(value)) = kwargs.get_item("value_not") { @@ -745,8 +861,6 @@ pub struct PyData { pub(crate) data: Vec<(AnnotationDataSetHandle, AnnotationDataHandle)>, pub(crate) store: Arc>, pub(crate) cursor: usize, - /// Indicates whether the annotations are sorted chronologically (by handle) - pub(crate) sorted: bool, } #[pymethods] @@ -785,73 +899,117 @@ impl PyData { !pyself.data.is_empty() } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|data, store| { - let iter = Data::from_handles(Cow::Borrowed(data), self.sorted, store) - .iter() - .annotations(); - iterparams.evaluate_to_pyannotations(iter, store, &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|data, store| { + Ok(PyAnnotations::from_iter( + data.items().annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |query, store| { + Ok(PyAnnotations::from_query(query, store, &self.store, limit)) + }) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|data, store| { - let iter = Data::from_handles(Cow::Borrowed(data), self.sorted, store) - .iter() - .annotations(); - Ok(iterparams.evaluate_annotations(iter, store)?.test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|data, _| Ok(data.items().annotations().test())) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |query, store| { + Ok(store.query(query).test()) + }) + } } } impl PyData { + pub(crate) fn from_iter<'store>( + iter: impl Iterator>, + wrappedstore: &Arc>, + ) -> Self { + Self { + data: iter + .map(|item| (item.set().handle(), item.handle())) + .collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + + pub(crate) fn from_query<'store>( + query: Query<'store>, + store: &'store AnnotationStore, + wrappedstore: &Arc>, + limit: Option, + ) -> Self { + assert!(query.resulttype() == Some(Type::AnnotationData)); + Self { + data: store + .query(query) + .limit(limit) + .map(|mut resultitems| { + //we use the deepest item if there are multiple + if let Some(QueryResultItem::AnnotationData(data)) = resultitems.pop_last() { + (data.set().handle(), data.handle()) + } else { + unreachable!("Unexpected QueryResultItem"); + } + }) + .collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + fn map(&self, f: F) -> Result where - F: FnOnce( - &Vec<(AnnotationDataSetHandle, AnnotationDataHandle)>, - &AnnotationStore, - ) -> Result, + F: FnOnce(Handles, &AnnotationStore) -> Result, { if let Ok(store) = self.store.read() { - f(&self.data, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) + let handles = Data::new(Cow::Borrowed(&self.data), false, &store); + f(handles, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) } else { Err(PyRuntimeError::new_err( "Unable to obtain store (should never happen)", )) } } -} -impl<'py> IterParams<'py> { - pub(crate) fn evaluate_to_pydata<'store>( - self, - iter: DataIter<'store>, - store: &'store AnnotationStore, - wrappedstore: &Arc>, - ) -> Result + fn map_with_query( + &self, + resulttype: Type, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result where - 'py: 'store, + F: FnOnce(Query, &AnnotationStore) -> Result, { - let limit = self.limit(); - match self.evaluate_data(iter, store) { - Ok(iter) => { - let sorted = iter.returns_sorted(); - Ok(PyData { - data: if let Some(limit) = limit { - iter.to_collection_limit(limit).take() - } else { - iter.to_collection().take() - }, - store: wrappedstore.clone(), - cursor: 0, - sorted, - }) - } - Err(e) => Err(e.into()), - } + self.map(|data, store| { + let query = Query::new(QueryType::Select, Some(Type::AnnotationData), Some("main")) + .with_constraint(Constraint::Data(data, SelectionQualifier::Normal)) + .with_subquery( + build_query( + Query::new(QueryType::Select, Some(resulttype), Some("sub")) + .with_constraint(Constraint::DataVariable( + "main", + SelectionQualifier::Normal, + )), + args, + kwargs, + store, + ) + .map_err(|e| { + StamError::QuerySyntaxError(format!("{}", e), "(python to query)") + })?, + ); + f(query, store) + }) } } diff --git a/src/annotationdataset.rs b/src/annotationdataset.rs index c075c79..02d9636 100644 --- a/src/annotationdataset.rs +++ b/src/annotationdataset.rs @@ -7,7 +7,7 @@ use std::sync::{Arc, RwLock}; use crate::annotationdata::{datavalue_from_py, PyAnnotationData, PyData, PyDataKey}; use crate::error::PyStamError; -use crate::iterparams::IterParams; +use crate::query::*; use crate::selector::{PySelector, PySelectorKind}; use stam::*; @@ -172,22 +172,42 @@ impl PyAnnotationDataSet { }) } - #[pyo3(signature = (**kwargs))] - fn data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|dataset| { - let iter = dataset.data(); - iterparams.evaluate_to_pydata(iter, dataset.store(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|dataset| Ok(PyData::from_iter(dataset.data().limit(limit), &self.store))) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::DataSetVariable("main", SelectionQualifier::Normal), + args, + kwargs, + |dataset, query| { + Ok(PyData::from_query( + query, + dataset.store(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|dataset| { - let iter = dataset.data(); - Ok(iterparams.evaluate_data(iter, dataset.store())?.test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|dataset| Ok(dataset.data().test())) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::DataSetVariable("main", SelectionQualifier::Normal), + args, + kwargs, + |dataset, query| Ok(dataset.store().query(query).test()), + ) + } } /// Returns a Selector (DataSetSelector) pointing to this AnnotationDataSet @@ -200,6 +220,8 @@ impl PyAnnotationDataSet { dataset: Some(dataset.handle()), annotation: None, resource: None, + key: None, + data: None, offset: None, subselectors: Vec::new(), }) @@ -241,6 +263,31 @@ impl PyAnnotationDataSet { )) } } + + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(ResultItem, Query) -> Result, + { + self.map(|dataset| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(constraint), + args, + kwargs, + dataset.store(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_datasetvar("main", dataset.clone()); + f(dataset, query) + }) + } } #[pyclass(name = "DataKeyIter")] diff --git a/src/annotationstore.rs b/src/annotationstore.rs index f431305..097133f 100644 --- a/src/annotationstore.rs +++ b/src/annotationstore.rs @@ -9,8 +9,7 @@ use crate::annotationdata::{annotationdata_builder, data_request_parser, PyData} use crate::annotationdataset::PyAnnotationDataSet; use crate::config::get_config; use crate::error::PyStamError; -use crate::get_limit; -use crate::iterparams::IterParams; +use crate::query::*; use crate::resources::PyTextResource; use crate::selector::PySelector; use stam::*; @@ -247,13 +246,21 @@ impl PyAnnotationStore { }) } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|store| { - let iter = store.annotations(); - iterparams.evaluate_to_pyannotations(iter, store, &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|store| { + Ok(PyAnnotations::from_iter( + store.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query(Type::Annotation, args, kwargs, |query, store| { + Ok(PyAnnotations::from_query(query, store, &self.store, limit)) + }) + } } /// Returns a generator over all annotations in this store @@ -266,6 +273,7 @@ impl PyAnnotationStore { /// Returns a generator over all resources in this store fn resources(&self) -> PyResult { + //TODO: transform to PyResources Ok(PyResourceIter { store: self.store.clone(), index: 0, @@ -291,30 +299,16 @@ impl PyAnnotationStore { self.map_mut(|store| Ok(store.shrink_to_fit(true))) } - /// Find annotation data for the specified set, key and value - /// Returns all found AnnotationData instances - #[pyo3(signature = (**kwargs))] - fn find_data(&self, kwargs: Option<&PyDict>) -> PyResult { + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { let limit = get_limit(kwargs); - self.map( - |store| match data_request_parser(kwargs, store, None, None) { - Ok((sethandle, keyhandle, op)) => { - let iter = store.find_data(sethandle, keyhandle, op); - let sorted = iter.returns_sorted(); - Ok(PyData { - data: if let Some(limit) = limit { - iter.to_collection_limit(limit).take() - } else { - iter.to_collection().take() - }, - store: self.store.clone(), - cursor: 0, - sorted, - }) - } - Err(e) => Err(e), - }, - ) + if !has_filters(args, kwargs) { + self.map(|store| Ok(PyData::from_iter(store.data().limit(limit), &self.store))) + } else { + self.map_with_query(Type::AnnotationData, args, kwargs, |query, store| { + Ok(PyData::from_query(query, store, &self.store, limit)) + }) + } } } @@ -374,6 +368,28 @@ impl PyAnnotationStore { { self.map_store_mut(f) } + + fn map_with_query( + &self, + resulttype: Type, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(Query, &AnnotationStore) -> Result, + { + self.map_store(|store| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), None), + args, + kwargs, + store, + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))?; + f(query, store) + }) + } } #[pyclass(name = "AnnotationIter")] diff --git a/src/iterparams.rs b/src/iterparams.rs deleted file mode 100644 index e10c3a3..0000000 --- a/src/iterparams.rs +++ /dev/null @@ -1,311 +0,0 @@ -use pyo3::exceptions::PyValueError; -use pyo3::prelude::*; -use pyo3::types::*; -use std::borrow::Cow; - -use crate::annotation::{PyAnnotation, PyAnnotations}; -use crate::annotationdata::{dataoperator_from_kwargs, PyAnnotationData, PyData, PyDataKey}; -use crate::error::PyStamError; -use crate::textselection::PyTextSelectionOperator; -use stam::*; - -pub enum Filter<'a> { - Annotation(AnnotationHandle), - Annotations((Vec, bool)), //the boolean expresses whether the data is sorted or not - AnnotationData(AnnotationDataSetHandle, AnnotationDataHandle), - Data((Vec<(AnnotationDataSetHandle, AnnotationDataHandle)>, bool)), //the boolean expresses whether the data is sorted or not - DataKey(AnnotationDataSetHandle, DataKeyHandle), - Value(DataOperator<'a>), - TextRelation(TextSelectionOperator), - //FindData(AnnotationDataSetHandle, DataKeyHandle, DataOperator<'a>), -} - -pub struct IterParams<'a> { - filters: Vec>, - limit: Option, -} - -fn add_filter<'py>(filters: &mut Vec>, filter: &'py PyAny) -> PyResult<()> { - if filter.is_instance_of::() { - let vec: Vec<&PyAny> = filter.extract()?; - add_multi_filter(filters, vec)?; - } else if filter.is_instance_of::() { - let vec: Vec<&PyAny> = filter.extract()?; - add_multi_filter(filters, vec)?; - } else if filter.is_instance_of::() { - let adata: PyRef<'_, PyAnnotationData> = filter.extract()?; - filters.push(Filter::AnnotationData(adata.set, adata.handle)); - } else if filter.is_instance_of::() { - let key: PyRef<'_, PyDataKey> = filter.extract()?; - filters.push(Filter::DataKey(key.set, key.handle)); - } else if filter.is_instance_of::() { - let annotation: PyRef<'_, PyAnnotation> = filter.extract()?; - filters.push(Filter::Annotation(annotation.handle)); - } else if filter.is_instance_of::() { - let operator: PyRef<'_, PyTextSelectionOperator> = filter.extract()?; - filters.push(Filter::TextRelation(operator.operator)); - } else if filter.is_instance_of::() { - let annotations: PyRef<'py, PyAnnotations> = filter.extract()?; - filters.push(Filter::Annotations(( - annotations.annotations.iter().copied().collect(), - annotations.sorted, - ))); - } else if filter.is_instance_of::() { - let data: PyRef<'_, PyData> = filter.extract()?; - filters.push(Filter::Data(( - data.data.iter().copied().collect(), - data.sorted, - ))); - } else { - return Err(PyValueError::new_err( - "Got argument of unexpected type for filter=/filters=", - )); - } - Ok(()) -} - -fn add_multi_filter<'a>(filters: &mut Vec>, filter: Vec<&PyAny>) -> PyResult<()> { - if filter.iter().all(|x| x.is_instance_of::()) { - filters.push(Filter::Annotations(( - filter - .iter() - .map(|x| { - let annotation: PyRef<'_, PyAnnotation> = x.extract().unwrap(); - annotation.handle - }) - .collect(), - false, //we don't know if the data is sorted - ))); - } else if filter - .iter() - .all(|x| x.is_instance_of::()) - { - filters.push(Filter::Data(( - filter - .iter() - .map(|x| { - let adata: PyRef<'_, PyAnnotationData> = x.extract().unwrap(); - (adata.set, adata.handle) - }) - .collect(), - false, //we don't know if the data is sorted - ))); - } - Ok(()) -} - -impl<'py> IterParams<'py> { - pub fn new(kwargs: Option<&'py PyDict>) -> PyResult { - let mut filters = Vec::new(); - let mut limit: Option = None; - if let Some(kwargs) = kwargs { - if let Ok(Some(v)) = kwargs.get_item("limit") { - match v.extract() { - Ok(v) => limit = v, - Err(e) => { - return Err(PyValueError::new_err(format!( - "Limit should be an integer or None: {}", - e - ))) - } - } - } - if let Ok(Some(filter)) = kwargs.get_item("filter") { - add_filter(&mut filters, filter)?; - } else if let Ok(Some(filter)) = kwargs.get_item("filters") { - if filter.is_instance_of::() { - let vec = filter.downcast::()?; - for filter in vec { - add_filter(&mut filters, filter)?; - } - } else if filter.is_instance_of::() { - let vec = filter.downcast::()?; - for filter in vec { - add_filter(&mut filters, filter)?; - } - } - } - if let Some(operator) = dataoperator_from_kwargs(kwargs) - .map_err(|e| PyStamError::new_err(format!("{}", e)))? - { - filters.push(Filter::Value(operator)); - } - } - Ok(Self { filters, limit }) - } - - pub fn limit(&self) -> Option { - self.limit - } - - pub fn evaluate_annotations<'store>( - self, - mut iter: stam::AnnotationsIter<'store>, - store: &'store AnnotationStore, - ) -> Result, StamError> - where - 'py: 'store, - { - let mut has_value_filter = false; - let mut datakey_filter: Option> = None; - for filter in self.filters.iter() { - if let Filter::Value(op) = filter { - if let DataOperator::Any = op { - //ignore - } else { - has_value_filter = true; - } - } - } - for filter in self.filters.into_iter() { - match filter { - Filter::Annotation(handle) => { - iter = iter.filter_handle(handle); - } - Filter::AnnotationData(set_handle, data_handle) => { - iter = iter.filter_annotationdata_handle(set_handle, data_handle); - } - Filter::DataKey(set_handle, key_handle) => { - let dataset = store - .dataset(set_handle) - .ok_or_else(|| StamError::HandleError("Unable to find dataset"))?; - let key = dataset - .key(key_handle) - .ok_or_else(|| StamError::HandleError("Unable to find key"))?; - //check if we also have a value filter - if !has_value_filter { - iter = iter.filter_data(key.data().to_collection()); - } else { - datakey_filter = Some(key); //will be handled further in Filter::Value arm - } - } - Filter::Annotations((annotations, sorted)) => { - let annotations = - Annotations::from_handles(Cow::Owned(annotations), sorted, store); - iter = iter.filter_annotations(annotations.iter()); - } - Filter::Data((data, sorted)) => { - let data = Data::from_handles(Cow::Owned(data), sorted, store); - iter = iter.filter_data(data); - } - Filter::Value(op) => { - if let Some(key) = &datakey_filter { - iter = iter.filter_data(key.data().filter_value(op).to_collection()); - } else { - return Err(StamError::OtherError( - "Python: You can specify a value filter only if you pass filter=DataKey (Annotations)", - )); - } - } - Filter::TextRelation(op) => { - iter = iter.filter_related_text(op); - } - } - } - Ok(iter) - } - - pub fn evaluate_data<'store>( - self, - mut iter: stam::DataIter<'store>, - store: &'store AnnotationStore, - ) -> Result, StamError> - where - 'py: 'store, - { - for filter in self.filters.into_iter() { - match filter { - Filter::Annotation(handle) => { - if let Some(annotation) = store.annotation(handle) { - iter = iter.filter_annotation(&annotation); - } - } - Filter::Annotations((annotations, sorted)) => { - let annotations = - Annotations::from_handles(Cow::Owned(annotations), sorted, store); - iter = iter.filter_annotations(annotations.iter()); - } - Filter::Data((data, sorted)) => { - let data = Data::from_handles(Cow::Owned(data), sorted, store); - iter = iter.filter_data(data.iter()); - } - Filter::DataKey(set_handle, key_handle) => { - iter = iter.filter_key_handle(set_handle, key_handle); - } - Filter::Value(operator) => { - iter = iter.filter_value(operator.clone()); - } - _ => { - return Err(StamError::OtherError( - "Python: not a valid filter in this context (Data)", - )); - } - } - } - Ok(iter) - } - - pub fn evaluate_textselections<'store>( - self, - mut iter: stam::TextSelectionsIter<'store>, - store: &'store AnnotationStore, - ) -> Result, StamError> - where - 'py: 'store, - { - let mut has_value_filter = false; - let mut datakey_filter: Option> = None; - for filter in self.filters.iter() { - if let Filter::Value(op) = filter { - if let DataOperator::Any = op { - //ignore - } else { - has_value_filter = true; - } - } - } - for filter in self.filters.into_iter() { - match filter { - Filter::TextRelation(op) => { - iter = iter.related_text(op); - } - Filter::Annotation(handle) => { - iter = iter.filter_annotation_handle(handle); - } - Filter::Annotations((annotations, sorted)) => { - let annotations = - Annotations::from_handles(Cow::Owned(annotations), sorted, store); - iter = iter.filter_annotations(annotations); - } - Filter::AnnotationData(set_handle, data_handle) => { - iter = iter.filter_annotationdata_handle(set_handle, data_handle); - } - Filter::Data((data, sorted)) => { - let data = Data::from_handles(Cow::Owned(data), sorted, store); - iter = iter.filter_data(data); - } - Filter::DataKey(set_handle, key_handle) => { - if let Some(dataset) = store.dataset(set_handle) { - if let Some(key) = dataset.key(key_handle) { - if !has_value_filter { - iter = iter.filter_data(key.data().to_collection()); - } else { - datakey_filter = Some(key); //will be handled further in Filter::Value arm - } - } - } - } - Filter::Value(operator) => { - if let Some(key) = &datakey_filter { - iter = iter.filter_data(key.data().filter_value(operator).to_collection()); - } else { - return Err(StamError::OtherError( - "Python: You can specify a value filter only if you pass filter=DataKey (TextSelections)", - )); - } - } - } - } - Ok(iter) - } -} diff --git a/src/lib.rs b/src/lib.rs index d85af2d..b4322c2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ mod annotationstore; mod config; mod error; //mod query; -mod iterparams; +mod query; mod resources; mod selector; mod textselection; @@ -47,14 +47,3 @@ fn stam(py: Python<'_>, m: &PyModule) -> PyResult<()> { m.add_class::()?; Ok(()) } - -pub(crate) fn get_limit(kwargs: Option<&PyDict>) -> Option { - if let Some(kwargs) = kwargs { - if let Ok(Some(limit)) = kwargs.get_item("limit") { - if let Ok(limit) = limit.extract::() { - return Some(limit); - } - } - } - None -} diff --git a/src/query.rs b/src/query.rs new file mode 100644 index 0000000..27dae53 --- /dev/null +++ b/src/query.rs @@ -0,0 +1,390 @@ +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use pyo3::types::*; +use std::collections::HashSet; + +use crate::annotation::{PyAnnotation, PyAnnotations}; +use crate::annotationdata::{dataoperator_from_kwargs, PyAnnotationData, PyData, PyDataKey}; +use crate::annotationdataset::PyAnnotationDataSet; +use crate::error::PyStamError; +use stam::*; + +const CONTEXTVARNAMES: [&str; 25] = [ + "v1", "v2", "v3", "v4", "v5", "v6", "v7", "v8", "v9", "v10", "v11", "v12", "v13", "v14", "v15", + "v16", "v17", "v18", "v19", "v20", "v21", "v22", "v23", "v24", "v25", +]; + +fn new_contextvar(used_contextvarnames: &mut usize) -> &'static str { + let varname = CONTEXTVARNAMES + .get(*used_contextvarnames) + .map(|x| *x) + .expect("no free context variables present"); + *used_contextvarnames += 1; + varname +} + +fn add_filter<'store, 'py, 'context>( + query: &mut Query<'store>, + store: &'store AnnotationStore, + filter: &'py PyAny, + operator: Option>, + mut used_contextvarnames: usize, +) -> PyResult +where + 'py: 'store, + 'context: 'store, +{ + if filter.is_instance_of::() { + let filter: &PyDict = filter.extract()?; + let operator = dataoperator_from_kwargs(filter) + .map_err(|err| PyValueError::new_err(format!("{}", err)))? + .or(operator); + if filter.contains("key")? { + let key = filter + .get_item("key")? + .expect("key field was checked to exist in filter"); + if key.is_instance_of::() { + let key: PyRef<'py, PyDataKey> = filter.extract()?; + if let Some(key) = store.key(key.set, key.handle) { + let varname = new_contextvar(&mut used_contextvarnames); + query.bind_keyvar(varname, key); + if let Some(operator) = operator { + query.constrain(Constraint::KeyValueVariable( + varname, + operator, + SelectionQualifier::Normal, + )); + } else { + query.constrain(Constraint::KeyVariable( + varname, + SelectionQualifier::Normal, + )); + } + } else { + return Err(PyValueError::new_err( + "Passed DataKey instance is invalid (should never happen)", + )); + } + } else if filter.contains("set")? { + if key.is_instance_of::() { + let key = key.downcast::()?; + let set = filter.get_item("set")?.expect("already checked"); + let key = if set.is_instance_of::() { + let set: PyRef<'py, PyAnnotationDataSet> = filter.extract()?; + if let Some(dataset) = store.dataset(set.handle) { + if let Some(key) = dataset.key(key.to_str()?) { + Some(key) + } else { + return Err(PyValueError::new_err( + "specified key not found in set", + )); + } + } else { + return Err(PyValueError::new_err( + "Passed AnnotationDataSet instance is invalid (should never happen)", + )); + } + } else if set.is_instance_of::() { + let set = set.downcast::()?; + if let Some(dataset) = store.dataset(set.to_str()?) { + if let Some(key) = dataset.key(key.to_str()?) { + Some(key) + } else { + return Err(PyValueError::new_err( + "specified key not found in set", + )); + } + } else { + return Err(PyValueError::new_err("specified dataset not found")); + } + } else { + None + }; + if let Some(key) = key { + let varname = new_contextvar(&mut used_contextvarnames); + query.bind_keyvar(varname, key); + if let Some(operator) = operator { + query.constrain(Constraint::KeyValueVariable( + varname, + operator, + SelectionQualifier::Normal, + )); + } else { + query.constrain(Constraint::KeyVariable( + varname, + SelectionQualifier::Normal, + )); + } + } + } else { + return Err(PyValueError::new_err( + "'key' parameter in filter dictionary should be of type `str`", + )); + } + } + } else if let Some(operator) = operator { + //no key specified but we do have an operator + query.constrain(Constraint::Value(operator, SelectionQualifier::Normal)); + } + } else if filter.is_instance_of::() { + let vec: Vec<&PyAny> = filter.extract()?; + used_contextvarnames = add_multi_filter(query, store, vec, used_contextvarnames)?; + } else if filter.is_instance_of::() { + let vec: Vec<&PyAny> = filter.extract()?; + used_contextvarnames = add_multi_filter(query, store, vec, used_contextvarnames)?; + } else if filter.is_instance_of::() { + let data: PyRef<'_, PyAnnotationData> = filter.extract()?; + if operator.is_some() { + return Err(PyValueError::new_err( + "'value' parameter can not be used in combination with an AnnotationData instance (it already restrains to a single value)", + )); + } + if let Some(data) = store.annotationdata(data.set, data.handle) { + let varname = new_contextvar(&mut used_contextvarnames); + query.bind_datavar(varname, data); + query.constrain(Constraint::DataVariable( + varname, + SelectionQualifier::Normal, + )); + } + } else if filter.is_instance_of::() { + let key: PyRef<'py, PyDataKey> = filter.extract()?; + if let Some(key) = store.key(key.set, key.handle) { + let varname = new_contextvar(&mut used_contextvarnames); + query.bind_keyvar(varname, key); + if let Some(operator) = operator { + query.constrain(Constraint::KeyValueVariable( + varname, + operator, + SelectionQualifier::Normal, + )); + } else { + query.constrain(Constraint::KeyVariable(varname, SelectionQualifier::Normal)); + } + } else { + return Err(PyValueError::new_err( + "Passed DataKey instance is invalid (should never happen)", + )); + } + } else if filter.is_instance_of::() { + let annotation: PyRef<'py, PyAnnotation> = filter.extract()?; + if let Some(annotation) = store.annotation(annotation.handle) { + let varname = new_contextvar(&mut used_contextvarnames); + query.bind_annotationvar(varname, annotation); + query.constrain(Constraint::AnnotationVariable( + varname, + SelectionQualifier::Normal, + AnnotationDepth::One, + )); + } else { + return Err(PyValueError::new_err( + "Passed DataKey instance is invalid (should never happen)", + )); + } + } else if filter.is_instance_of::() { + let annotations: PyRef<'py, PyAnnotations> = filter.extract()?; + query.constrain(Constraint::Annotations( + Handles::from_iter(annotations.annotations.iter().copied(), store), + SelectionQualifier::Normal, + AnnotationDepth::One, + )); + } else if filter.is_instance_of::() { + let data: PyRef<'py, PyData> = filter.extract()?; + query.constrain(Constraint::Data( + Handles::from_iter(data.data.iter().copied(), store), + SelectionQualifier::Normal, + )); + } else { + return Err(PyValueError::new_err( + "Got filter argument of unexpected type", + )); + } + Ok(used_contextvarnames) +} + +fn add_multi_filter<'a>( + query: &mut Query<'a>, + store: &'a AnnotationStore, + filter: Vec<&'a PyAny>, + mut used_contextvarnames: usize, +) -> PyResult { + if filter.iter().all(|x| x.is_instance_of::()) { + query.constrain(Constraint::Annotations( + Handles::from_iter( + filter.iter().map(|x| { + let annotation: PyRef<'_, PyAnnotation> = x.extract().unwrap(); + annotation.handle + }), + store, + ), + SelectionQualifier::Normal, + AnnotationDepth::One, + )); + } else if filter + .iter() + .all(|x| x.is_instance_of::()) + { + query.constrain(Constraint::Data( + Handles::from_iter( + filter.iter().map(|x| { + let adata: PyRef<'_, PyAnnotationData> = x.extract().unwrap(); + (adata.set, adata.handle) + }), + store, + ), + SelectionQualifier::Normal, + )); + } else { + for item in filter.iter() { + used_contextvarnames = add_filter(query, store, item, None, used_contextvarnames)?; + } + } + Ok(used_contextvarnames) +} + +pub(crate) fn build_query<'store, 'py>( + mut query: Query<'store>, + args: &'py PyTuple, + kwargs: Option<&'py PyDict>, + store: &'store AnnotationStore, +) -> PyResult> +where + 'py: 'store, +{ + let mut used_contextvarnames: usize = 0; + let operator = if let Some(kwargs) = kwargs { + dataoperator_from_kwargs(kwargs).map_err(|e| PyStamError::new_err(format!("{}", e)))? + } else { + None + }; + let mut has_args = false; + for filter in args { + has_args = true; + used_contextvarnames = add_filter( + &mut query, + store, + filter, + operator.clone(), + used_contextvarnames, + )?; + } + if let Some(kwargs) = kwargs { + if let Ok(Some(filter)) = kwargs.get_item("filter") { + //backwards compatibility + add_filter(&mut query, store, filter, operator, used_contextvarnames)?; + } else if let Ok(Some(filter)) = kwargs.get_item("filters") { + //backwards compatibility + if filter.is_instance_of::() { + let vec = filter.downcast::()?; + for filter in vec { + used_contextvarnames = add_filter( + &mut query, + store, + filter, + operator.clone(), + used_contextvarnames, + )?; + } + } else if filter.is_instance_of::() { + let vec = filter.downcast::()?; + for filter in vec { + used_contextvarnames = add_filter( + &mut query, + store, + filter, + operator.clone(), + used_contextvarnames, + )?; + } + } + } else if !has_args { + //we have no args, handle kwargs standalone + add_filter( + &mut query, + store, + kwargs.as_ref(), + None, + used_contextvarnames, + )?; + } + } + Ok(query) +} + +pub(crate) fn has_filters(args: &PyTuple, kwargs: Option<&PyDict>) -> bool { + if !args.is_empty() { + return true; + } + if let Some(kwargs) = kwargs { + for key in kwargs.keys() { + if let Ok(Some("limit")) | Ok(Some("recursive")) = key.extract() { + continue; //this doesn't count as a filter + } else { + return true; + } + } + } + false +} + +pub(crate) fn get_recursive(kwargs: Option<&PyDict>, default: AnnotationDepth) -> AnnotationDepth { + if let Some(kwargs) = kwargs { + if let Ok(Some(v)) = kwargs.get_item("recursive") { + if let Ok(v) = v.extract::() { + if v { + return AnnotationDepth::Max; + } else { + return AnnotationDepth::One; + } + } + } + } + default +} + +pub(crate) fn get_limit(kwargs: Option<&PyDict>) -> Option { + if let Some(kwargs) = kwargs { + if let Ok(Some(limit)) = kwargs.get_item("limit") { + if let Ok(limit) = limit.extract::() { + return Some(limit); + } + } + } + None +} + +pub(crate) struct LimitIter { + inner: I, + limit: Option, +} + +impl Iterator for LimitIter +where + I: Iterator, +{ + type Item = I::Item; + fn next(&mut self) -> Option { + if let Some(remainder) = self.limit.as_mut() { + if *remainder > 0 { + *remainder -= 1; + self.inner.next() + } else { + None + } + } else { + self.inner.next() + } + } +} + +pub(crate) trait LimitIterator +where + Self: Iterator, + Self: Sized, +{ + fn limit(self, limit: Option) -> LimitIter { + LimitIter { inner: self, limit } + } +} + +impl LimitIterator for I where I: Iterator {} diff --git a/src/resources.rs b/src/resources.rs index 48c57c4..f3a69fd 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -7,7 +7,7 @@ use std::sync::{Arc, RwLock}; use crate::annotation::PyAnnotations; use crate::error::PyStamError; -use crate::iterparams::IterParams; +use crate::query::*; use crate::selector::{PySelector, PySelectorKind}; use crate::textselection::{ PyTextSelection, PyTextSelectionIter, PyTextSelectionOperator, PyTextSelections, @@ -302,6 +302,8 @@ impl PyTextResource { resource: Some(resource.handle()), annotation: None, dataset: None, + key: None, + data: None, offset: None, subselectors: Vec::new(), }) @@ -310,12 +312,10 @@ impl PyTextResource { fn textselections(&self) -> PyResult { self.map(|resource| { - let iter = resource.textselections(); - Ok(PyTextSelections { - textselections: iter.to_handles(), - store: self.store.clone(), - cursor: 0, - }) + Ok(PyTextSelections::from_iter( + resource.textselections(), + &self.store.clone(), + )) }) } @@ -379,67 +379,102 @@ impl PyTextResource { self.map(|res| res.beginaligned_cursor(&Cursor::EndAligned(endalignedcursor))) } - /// Returns all annotations (:obj:`Annotation`) that reference this resource via either a TextSelector or a ResourceSelector (if any). - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations(); - iterparams.evaluate_to_pyannotations(iter, resource.store(), &self.store) - }) - } - - #[pyo3(signature = (**kwargs))] - fn annotations_as_metadata(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations_as_metadata(); - iterparams.evaluate_to_pyannotations(iter, resource.store(), &self.store) - }) - } - - #[pyo3(signature = (**kwargs))] - fn annotations_on_text(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations_on_text(); - iterparams.evaluate_to_pyannotations(iter, resource.store(), &self.store) - }) + /// Returns annotations that are referring to this resource via a TextSelector + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|resource| { + Ok(PyAnnotations::from_iter( + resource.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::ResourceVariable("main", SelectionQualifier::Normal), + args, + kwargs, + |annotation, query| { + Ok(PyAnnotations::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations(); - Ok(iterparams - .evaluate_annotations(iter, resource.store())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations_as_metadata( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|resource| { + Ok(PyAnnotations::from_iter( + resource.annotations_as_metadata().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::ResourceVariable("main", SelectionQualifier::Metadata), + args, + kwargs, + |annotation, query| { + Ok(PyAnnotations::from_query( + query, + annotation.store(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations_as_metadata(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations_as_metadata(); - Ok(iterparams - .evaluate_annotations(iter, resource.store())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|resource| Ok(resource.annotations().test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::ResourceVariable("main", SelectionQualifier::Normal), + args, + kwargs, + |resource, query| Ok(resource.store().query(query).test()), + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations_on_text(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|resource| { - let iter = resource.annotations_on_text(); - Ok(iterparams - .evaluate_annotations(iter, resource.store())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations_as_metadata( + &self, + args: &PyTuple, + kwargs: Option<&PyDict>, + ) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|resource| Ok(resource.annotations_as_metadata().test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::ResourceVariable("main", SelectionQualifier::Metadata), + args, + kwargs, + |resource, query| Ok(resource.store().query(query).test()), + ) + } } + /* /// Applies a `TextSelectionOperator` to find all other text selections that are in a specific /// relation with the reference selections (a list of :obj:`TextSelection` instances). Returns /// all matching TextSelections in a list @@ -451,7 +486,6 @@ impl PyTextResource { referenceselections: Vec, kwargs: Option<&PyDict>, ) -> PyResult { - let iterparams = IterParams::new(kwargs)?; self.map(|textselection| { let mut refset = TextSelectionSet::new(self.handle); refset.extend(referenceselections.into_iter().map(|x| x.textselection)); @@ -459,6 +493,7 @@ impl PyTextResource { iterparams.evaluate_to_pytextselections(iter, textselection.rootstore(), &self.store) }) } + */ } impl PyTextResource { @@ -478,6 +513,31 @@ impl PyTextResource { )) } } + + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(ResultItem, Query) -> Result, + { + self.map(|resource| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(constraint), + args, + kwargs, + resource.store(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_resourcevar("main", resource.clone()); + f(resource, query) + }) + } } #[pyclass(dict, module = "stam", name = "Cursor")] diff --git a/src/selector.rs b/src/selector.rs index 19e8508..4bbd430 100644 --- a/src/selector.rs +++ b/src/selector.rs @@ -3,6 +3,7 @@ use pyo3::prelude::*; use pyo3::pyclass::CompareOp; use crate::annotation::PyAnnotation; +use crate::annotationdata::{PyAnnotationData, PyDataKey}; use crate::annotationdataset::PyAnnotationDataSet; use crate::annotationstore::{MapStore, PyAnnotationStore}; use crate::resources::{PyOffset, PyTextResource}; @@ -63,6 +64,8 @@ pub(crate) struct PySelector { pub(crate) resource: Option, pub(crate) annotation: Option, pub(crate) dataset: Option, + pub(crate) key: Option<(AnnotationDataSetHandle, DataKeyHandle)>, + pub(crate) data: Option<(AnnotationDataSetHandle, AnnotationDataHandle)>, pub(crate) offset: Option, pub(crate) subselectors: Vec, } @@ -93,6 +96,26 @@ impl PySelector { .expect("pyselector of type datasetselector must have dataset, was checked on instantiation") .into(), ), + SelectorKind::DataKeySelector => SelectorBuilder::DataKeySelector( + self.key + .expect("pyselector of type datakeyselector must have key, was checked on instantiation") + .0 + .into(), + self.key + .expect("pyselector of type datakeyselector must have key, was checked on instantiation") + .1 + .into(), + ), + SelectorKind::AnnotationDataSelector => SelectorBuilder::AnnotationDataSelector( + self.data + .expect("pyselector of type annotationdataselector must have key, was checked on instantiation") + .0 + .into(), + self.data + .expect("pyselector of type annotationdataselector must have key, was checked on instantiation") + .1 + .into(), + ), SelectorKind::MultiSelector => { SelectorBuilder::MultiSelector(self.subselectors.iter().map(|subselector| subselector.build()).collect()) } @@ -110,12 +133,14 @@ impl PySelector { #[pymethods] impl PySelector { #[new] - #[pyo3(signature = (kind, resource=None, annotation=None, dataset=None, offset=None, subselectors=Vec::new()))] + #[pyo3(signature = (kind, resource=None, annotation=None, dataset=None, key=None, data=None, offset=None, subselectors=Vec::new()))] fn new( kind: &PySelectorKind, resource: Option>, annotation: Option>, dataset: Option>, + key: Option>, + data: Option>, offset: Option>, subselectors: Vec>, ) -> PyResult { @@ -127,6 +152,8 @@ impl PySelector { resource: Some(resource.handle), annotation: None, dataset: None, + key: None, + data: None, offset: None, subselectors: Vec::new(), }) @@ -142,6 +169,8 @@ impl PySelector { annotation: Some(annotation.handle), resource: None, dataset: None, + key: None, + data: None, offset: Some(offset.clone()), subselectors: Vec::new(), }) @@ -151,6 +180,8 @@ impl PySelector { annotation: Some(annotation.handle), resource: None, dataset: None, + key: None, + data: None, offset: None, subselectors: Vec::new(), }) @@ -167,6 +198,8 @@ impl PySelector { resource: Some(resource.handle), annotation: None, dataset: None, + key: None, + data: None, offset: Some(offset.clone()), subselectors: Vec::new(), }) @@ -184,6 +217,8 @@ impl PySelector { resource: None, annotation: None, dataset: Some(dataset.handle), + key: None, + data: None, offset: None, subselectors: Vec::new(), }) @@ -191,6 +226,38 @@ impl PySelector { Err(PyValueError::new_err("'dataset' keyword argument must be specified for DataSetSelector and point to an AnnotationDataSet instance")) } } + SelectorKind::DataKeySelector => { + if let Some(key) = key { + Ok(PySelector { + kind: kind.clone(), + resource: None, + annotation: None, + dataset: None, + key: Some((key.set, key.handle)), + data: None, + offset: None, + subselectors: Vec::new(), + }) + } else { + Err(PyValueError::new_err("'key' keyword argument must be specified for DataKeySelector and point to a DataKey instance")) + } + } + SelectorKind::AnnotationDataSelector => { + if let Some(data) = data { + Ok(PySelector { + kind: kind.clone(), + resource: None, + annotation: None, + dataset: None, + data: Some((data.set, data.handle)), + key: None, + offset: None, + subselectors: Vec::new(), + }) + } else { + Err(PyValueError::new_err("'key' keyword argument must be specified for DataKeySelector and point to a DataKey instance")) + } + } SelectorKind::MultiSelector | SelectorKind::CompositeSelector | SelectorKind::DirectionalSelector => { @@ -202,6 +269,8 @@ impl PySelector { resource: None, annotation: None, dataset: None, + key: None, + data: None, offset: None, subselectors: subselectors.into_iter().map(|sel| sel.clone()).collect(), }) @@ -221,6 +290,8 @@ impl PySelector { Some(resource), None, None, + None, + None, Some(offset), Vec::new(), ) @@ -237,6 +308,8 @@ impl PySelector { None, Some(annotation), None, + None, + None, offset, Vec::new(), ) @@ -251,6 +324,8 @@ impl PySelector { None, None, None, + None, + None, Vec::new(), ) } @@ -264,6 +339,8 @@ impl PySelector { None, Some(annotationset), None, + None, + None, Vec::new(), ) } @@ -278,6 +355,8 @@ impl PySelector { None, None, None, + None, + None, subselectors, ) } @@ -292,6 +371,8 @@ impl PySelector { None, None, None, + None, + None, subselectors, ) } @@ -306,6 +387,8 @@ impl PySelector { None, None, None, + None, + None, subselectors, ) } @@ -370,6 +453,28 @@ impl PySelector { }) } + /// Returns the key this selector points at, if any. + /// Works only for DataKeySelector, returns None otherwise. + /// Requires to explicitly pass the store so the resource can be found. + fn key(&self, store: PyRef) -> Option { + self.key.map(|(set_handle, key_handle)| PyDataKey { + set: set_handle, + handle: key_handle, + store: store.get_store().clone(), + }) + } + + /// Returns the annotationdata this selector points at, if any. + /// Works only for AnnotationDataSelector, returns None otherwise. + /// Requires to explicitly pass the store so the resource can be found. + fn annotationdata(&self, store: PyRef) -> Option { + self.data.map(|(set_handle, data_handle)| PyAnnotationData { + set: set_handle, + handle: data_handle, + store: store.get_store().clone(), + }) + } + /// Returns the annotation this selector points at, if any. /// Works only for AnnotationSelector, returns None otherwise. /// Requires to explicitly pass the store so the resource can be found. @@ -420,6 +525,14 @@ impl PySelector { Selector::AnnotationSelector(a_id, ..) => Some(*a_id), _ => None, }, + key: match selector { + Selector::DataKeySelector(set_id, key_id) => Some((*set_id, *key_id)), + _ => None, + }, + data: match selector { + Selector::AnnotationDataSelector(set_id, data_id) => Some((*set_id, *data_id)), + _ => None, + }, offset: selector.offset(store).map(|offset| PyOffset { offset }), subselectors: if selector.is_complex() { if let Some(subselectors) = selector.subselectors() { diff --git a/src/textselection.rs b/src/textselection.rs index be0172a..2063bf3 100644 --- a/src/textselection.rs +++ b/src/textselection.rs @@ -2,13 +2,15 @@ use pyo3::exceptions::{PyIndexError, PyRuntimeError}; use pyo3::prelude::*; use pyo3::pyclass::CompareOp; use pyo3::types::*; +use std::borrow::Cow; use std::hash::{Hash, Hasher}; use std::ops::FnOnce; use std::sync::{Arc, RwLock}; use crate::annotation::PyAnnotations; +use crate::annotationdata::PyData; use crate::error::PyStamError; -use crate::iterparams::IterParams; +use crate::query::*; use crate::resources::{PyOffset, PyTextResource}; use crate::textselection::TextSelectionHandle; use stam::*; @@ -294,48 +296,98 @@ impl PyTextSelection { }) } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselection| { - let iter = textselection.annotations(); - iterparams.evaluate_to_pyannotations(iter, textselection.rootstore(), &self.store) - }) + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|textselection| { + Ok(PyAnnotations::from_iter( + textselection.annotations().limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::TextVariable("main"), + args, + kwargs, + |textselection, query| { + Ok(PyAnnotations::from_query( + query, + textselection.rootstore(), + &self.store, + limit, + )) + }, + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselection| { - let iter = textselection.annotations(); - Ok(iterparams - .evaluate_annotations(iter, textselection.rootstore())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|textselection| Ok(textselection.annotations().test())) + } else { + self.map_with_query( + Type::Annotation, + Constraint::TextVariable("main"), + args, + kwargs, + |textselection, query| Ok(textselection.rootstore().query(query).test()), + ) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselection| { - let iter = textselection.annotations().data_unchecked(); - Ok(iterparams - .evaluate_data(iter, textselection.rootstore())? - .test()) - }) + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|textselection| Ok(textselection.annotations().data().test())) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::TextVariable("main"), + args, + kwargs, + |textselection, query| Ok(textselection.rootstore().query(query).test()), + ) + } } - #[pyo3(signature = (operator, **kwargs))] + #[pyo3(signature = (operator, *args, **kwargs))] fn related_text( &self, operator: PyTextSelectionOperator, + args: &PyTuple, kwargs: Option<&PyDict>, ) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselection| { - let iter = textselection.related_text(operator.operator); - iterparams.evaluate_to_pytextselections(iter, textselection.rootstore(), &self.store) - }) + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|textselection| { + Ok(PyTextSelections::from_iter( + textselection.related_text(operator.operator).limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::TextRelation { + var: "main", + operator: operator.operator, //MAYBE TODO: check if we need to invert an operator here? + }, + args, + kwargs, + |textselection, query| { + Ok(PyTextSelections::from_query( + query, + textselection.rootstore(), + &self.store, + limit, + )) + }, + ) + } } fn annotations_len(&self) -> usize { @@ -428,6 +480,31 @@ impl PyTextSelection { )) } } + + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result + where + F: FnOnce(ResultTextSelection, Query) -> Result, + { + self.map(|textselection| { + let query = build_query( + Query::new(QueryType::Select, Some(resulttype), Some("result")) + .with_constraint(constraint), + args, + kwargs, + textselection.rootstore(), + ) + .map_err(|e| StamError::QuerySyntaxError(format!("{}", e), "(python to query)"))? + .with_textvar("main", textselection.clone()); + f(textselection, query) + }) + } } impl From for TextSelection { @@ -453,21 +530,19 @@ impl PyTextSelections { fn __next__(mut pyself: PyRefMut<'_, Self>) -> Option { pyself.cursor += 1; //increment first (prevent exclusive mutability issues) pyself - .map( - |textselections: &Vec<(TextResourceHandle, TextSelectionHandle)>, store| { - //index is one ahead, prevents exclusive lock issues - if let Some((res_handle, handle)) = textselections.get(pyself.cursor - 1) { - let resource = store.get(*res_handle)?; - let textselection = resource.get(*handle)?; - return Ok(PyTextSelection::new( - textselection.clone(), - *res_handle, - &pyself.store, - )); - } - Err(StamError::HandleError("a handle did not resolve")) - }, - ) + .map(|textselections, store| { + //index is one ahead, prevents exclusive lock issues + if let Some((res_handle, handle)) = textselections.get(pyself.cursor - 1) { + let resource = store.get(res_handle)?; + let textselection = resource.get(handle)?; + return Ok(PyTextSelection::new( + textselection.clone(), + res_handle, + &pyself.store, + )); + } + Err(StamError::HandleError("a handle did not resolve")) + }) .ok() } @@ -503,81 +578,153 @@ impl PyTextSelections { } fn text_join(pyself: PyRef<'_, Self>, delimiter: &str) -> PyResult { - pyself.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, - ); - Ok(iter.text_join(delimiter)) + pyself.map(|textselections, _store| { + Ok(ResultTextSelections::new(textselections.items()).text_join(delimiter)) }) } fn text(pyself: PyRef<'_, Self>) -> PyResult> { - pyself.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, - ); - let v: Vec = iter.text().map(|s| s.to_string()).collect(); + pyself.map(|textselections, _store| { + let v: Vec = ResultTextSelections::new(textselections.items()) + .text() + .map(|s| s.to_string()) + .collect(); Ok(v) }) } - #[pyo3(signature = (**kwargs))] - fn annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, + #[pyo3(signature = (*args, **kwargs))] + fn annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|textselections, store| { + Ok(PyAnnotations::from_iter( + textselections + .items() + .map(|x| x.as_resulttextselection()) + .annotations() + .limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::TextVariable("main"), + args, + kwargs, + |query, store| Ok(PyAnnotations::from_query(query, store, &self.store, limit)), ) - .annotations(); - iterparams.evaluate_to_pyannotations(iter, store, &self.store) - }) + } } - #[pyo3(signature = (**kwargs))] - fn test_annotations(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, + #[pyo3(signature = (*args, **kwargs))] + fn test_annotations(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|annotations, _| { + Ok(annotations + .items() + .map(|x| x.as_resulttextselection()) + .annotations() + .test()) + }) + } else { + self.map_with_query( + Type::Annotation, + Constraint::TextVariable("main"), + args, + kwargs, + |query, store| Ok(store.query(query).test()), ) - .annotations(); - Ok(iterparams.evaluate_annotations(iter, store)?.test()) - }) + } } - #[pyo3(signature = (**kwargs))] - fn test_data(&self, kwargs: Option<&PyDict>) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, + #[pyo3(signature = (*args, **kwargs))] + fn data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|textselections, store| { + Ok(PyData::from_iter( + textselections + .items() + .map(|x| x.as_resulttextselection()) + .annotations() + .data() + .limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::TextVariable("main"), + args, + kwargs, + |query, store| Ok(PyData::from_query(query, store, &self.store, limit)), ) - .annotations_unchecked() - .data_unchecked(); - Ok(iterparams.evaluate_data(iter, store)?.test()) - }) + } + } + + #[pyo3(signature = (*args, **kwargs))] + fn test_data(&self, args: &PyTuple, kwargs: Option<&PyDict>) -> PyResult { + if !has_filters(args, kwargs) { + self.map(|textselections, _| { + Ok(textselections + .items() + .map(|x| x.as_resulttextselection()) + .annotations() + .data() + .test()) + }) + } else { + self.map_with_query( + Type::AnnotationData, + Constraint::TextVariable("main"), + args, + kwargs, + |query, store| Ok(store.query(query).test()), + ) + } } - #[pyo3(signature = (operator, **kwargs))] + #[pyo3(signature = (operator, *args, **kwargs))] fn related_text( &self, operator: PyTextSelectionOperator, + args: &PyTuple, kwargs: Option<&PyDict>, ) -> PyResult { - let iterparams = IterParams::new(kwargs)?; - self.map(|textselections, store| { - let iter = stam::TextSelectionsIter::from_handles( - textselections.iter().copied().collect(), //MAYBE TODO: work away the extra copy - store, + let limit = get_limit(kwargs); + if !has_filters(args, kwargs) { + self.map(|textselections, store| { + Ok(PyTextSelections::from_iter( + textselections + .items() + .map(|x| x.as_resulttextselection()) + .related_text(operator.operator) + .limit(limit), + &self.store, + )) + }) + } else { + self.map_with_query( + Type::TextSelection, + Constraint::TextRelation { + var: "main", + operator: operator.operator, //MAYBE TODO: check if we need to invert an operator here? + }, + args, + kwargs, + |query, store| { + Ok(PyTextSelections::from_query( + query, + store, + &self.store, + limit, + )) + }, ) - .related_text(operator.operator); - iterparams.evaluate_to_pytextselections(iter, store, &self.store) - }) + } } fn textual_order(mut pyself: PyRefMut<'_, Self>) -> PyRefMut<'_, Self> { @@ -608,15 +755,61 @@ impl PyTextSelections { } impl PyTextSelections { + pub(crate) fn from_iter<'store>( + iter: impl Iterator>, + wrappedstore: &Arc>, + ) -> Self { + Self { + textselections: iter + .map(|textselection| { + ( + textselection.resource().handle(), + textselection.handle().expect("textselection must be bound"), + ) + }) + .collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + + pub(crate) fn from_query<'store>( + query: Query<'store>, + store: &'store AnnotationStore, + wrappedstore: &Arc>, + limit: Option, + ) -> Self { + assert!(query.resulttype() == Some(Type::TextSelection)); + Self { + textselections: store + .query(query) + .limit(limit) + .map(|mut resultitems| { + //we use the deepest item if there are multiple + if let Some(QueryResultItem::TextSelection(textselection)) = + resultitems.pop_last() + { + ( + textselection.resource().handle(), + textselection.handle().expect("textselection must be bound"), + ) + } else { + unreachable!("Unexpected QueryResultItem"); + } + }) + .collect(), + store: wrappedstore.clone(), + cursor: 0, + } + } + fn map(&self, f: F) -> Result where - F: FnOnce( - &Vec<(TextResourceHandle, TextSelectionHandle)>, - &AnnotationStore, - ) -> Result, + F: FnOnce(TextSelections, &AnnotationStore) -> Result, { if let Ok(store) = self.store.read() { - f(&self.textselections, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) + let handles = TextSelections::new(Cow::Borrowed(&self.textselections), false, &store); + f(handles, &store).map_err(|err| PyStamError::new_err(format!("{}", err))) } else { Err(PyRuntimeError::new_err( "Unable to obtain store (should never happen)", @@ -640,31 +833,38 @@ impl PyTextSelections { )) } } -} -impl<'py> IterParams<'py> { - pub(crate) fn evaluate_to_pytextselections<'store>( - self, - iter: TextSelectionsIter<'store>, - store: &'store AnnotationStore, - wrappedstore: &Arc>, - ) -> Result + fn map_with_query( + &self, + resulttype: Type, + constraint: Constraint, + args: &PyTuple, + kwargs: Option<&PyDict>, + f: F, + ) -> Result where - 'py: 'store, + F: FnOnce(Query, &AnnotationStore) -> Result, { - let limit = self.limit(); - match self.evaluate_textselections(iter, store) { - Ok(iter) => Ok(PyTextSelections { - textselections: if let Some(limit) = limit { - iter.to_handles_limit(limit) - } else { - iter.to_handles() - }, - store: wrappedstore.clone(), - cursor: 0, - }), - Err(e) => Err(e.into()), - } + self.map(|textselections, store| { + let query = Query::new(QueryType::Select, Some(Type::Annotation), Some("main")) + .with_constraint(Constraint::TextSelections( + textselections, + SelectionQualifier::Normal, + )) + .with_subquery( + build_query( + Query::new(QueryType::Select, Some(resulttype), Some("sub")) + .with_constraint(constraint), + args, + kwargs, + store, + ) + .map_err(|e| { + StamError::QuerySyntaxError(format!("{}", e), "(python to query)") + })?, + ); + f(query, store) + }) } } diff --git a/stam.pyi b/stam.pyi index 3a36794..11ef41b 100644 --- a/stam.pyi +++ b/stam.pyi @@ -152,31 +152,33 @@ class AnnotationStore: or use the equally named method on other objects for more constrained and filterable annotations (e.g. :meth:`DataKey.annotations`, :meth:`AnnotationDataSet.annotations`, :meth:`TextResource.annotations`) """ - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """Returns an iterator over all annotations (:class:`Annotation`) in this store. - Filtering can be applied using keyword arguments. It is recommended to only use this method if you apply further filtering, otherwise the memory overhead may be very large if you have many annotations. + Filtering can be applied using positional arguments and/or keyword arguments. It is recommended to only use this method if you apply further filtering, otherwise the memory overhead may be very large if you have many annotations. Otherwise you can fall back to a more low-level iterator, :meth:`__iter__` instead + Positional Arguments + ------------------- + + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns annotations with data matching this key + * :class:`AnnotationData` - Returns only annotations that have this exact data + * :class:`Annotations` or a tuple/list of :class:`Annotation`- Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only annotations with data that is in the provided collection. + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` (see keyword arguments below) + Keyword Arguments ------------------- limit: Optional[int] = None The maximum number of results to return (default: unlimited) - filter: Union[AnnotationData,Tuple[AnnotationData],List[AnnotationData],DataKey,Annotations,Data,TextSelectionOperator] - If you want to add multiple different filters, use `filters` instead. - Filter annotations based on: - - * :class:`AnnotationData` - Returns only annotations that have this exact data (you can only pass this once). - * a tuple/list of :class:`AnnotationData` - Returns only annotations with data that matches one of the items in the tuple/list. - * :class:`DataKey` - Returns annotations with data matching this key (you can only pass this once). - * a tuple/list of :class:`DataKey` - * :class:`Annotations` - Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) - * :class:`Data` - Returns only annotations with data that is in the provided :obj:`Data` collection. - * :class:`TextSelectionOperator` - Returns only annotations that are in a particular textual relationship with the current one (e.g. overlap,embedding,adjacency,etc). - filters: List[Union[AnnotationData,DataKey,Annotations,Data]] value: Optional[Union[str,int,float,bool]] - Constrain the search to annotations with data of a certain value. This can only be used with `filter=DataKey`. + Constrain the search to annotations with data of a certain value. This can only be used when you also pass a :class:`DataKey` as filter. This holds the exact value to search for, there are other variants of this keyword available, see :meth:`data` for a full list. """ @@ -200,22 +202,36 @@ class AnnotationStore: def shrink_to_fit(self): """Reallocates internal data structures to tight fits to conserve memory space (if necessary). You can use this after having added lots of annotations to possibly reduce the memory consumption.""" - def find_data(self, **kwargs) -> Data: - """ - Find annotation data by set, key and value. - Returns :class:`Data`, which holds a collection of :class:`AnnotationData` instances. + def data(self, *args, **kwargs) -> Data: + """Returns an iterator over all data (:class:`AnnotationData`) in this store. - Keyword arguments + Filtering can be applied using positional arguments and/or keyword arguments. It is recommended to only use this method if you apply further filtering, otherwise the memory overhead may be very large if you have a lot of data. + + Positional Arguments ------------------- - set: Optional[Union[str,AnnotationDataSet]] - The set to search for; it will search all sets if not specified - key: Optional[Union[str,DataKey]] - The key to search for; it will search all keys if not specified. If you specify a key, you must also specify a set! - value: Optional[Union[str,int,float,bool]] - The exact value to search for, if this or any of its variants mentioned below is omitted, it will search all values. + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns data matching this key + * :class:`Annotation` - Returns data referenced by the mentioned annotation + * :class:`AnnotationData` - Returns only this exact data. Not very useful, use :meth:`test_data` instead. + * :class:`Annotations` or a tuple/list of :class:`Annotation` - Returns data references by annotations in the provided collection. + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only data that is in the provided :obj:`Data` collection (intersection) + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` or variants (see keyword arguments below) + + Keyword Arguments + ------------------- + + limit: Optional[int] = None + The maximum number of results to return (default: unlimited) + value: Optional[Union[str,int,float,bool,List[Union[str,int,float,bool]]]] + Search for data matching a specific value. + This holds exact value to search for. Further variants of this keyword are listed below: value_not: Optional[Union[str,int,float,bool]] - Value + Value must not match value_greater: Optional[Union[int,float]] Value must be greater than specified (int or float) value_less: Optional[Union[int,float]] @@ -230,35 +246,67 @@ class AnnotationStore: Value must not match any in the tuple value_in_range: Optional[Tuple[Union[int,float]]] Must be a numeric 2-tuple with min and max (inclusive) values + """ +# def find_data(self, **kwargs) -> Data: +# """ +# Find annotation data by set, key and value. +# Returns :class:`Data`, which holds a collection of :class:`AnnotationData` instances. - Examples - ------------- +# Keyword arguments +# ------------------- - Query for specific annotation data:: +# set: Optional[Union[str,AnnotationDataSet]] +# The set to search for; it will search all sets if not specified +# key: Optional[Union[str,DataKey]] +# The key to search for; it will search all keys if not specified. If you specify a key, you must also specify a set! +# value: Optional[Union[str,int,float,bool]] +# The exact value to search for, if this or any of its variants mentioned below is omitted, it will search all values. +# value_not: Optional[Union[str,int,float,bool]] +# Value +# value_greater: Optional[Union[int,float]] +# Value must be greater than specified (int or float) +# value_less: Optional[Union[int,float]] +# Value must be less than specified (int or float) +# value_greatereq: Optional[Union[int,float]] +# Value must be greater than specified or equal (int or float) +# value_lesseq: Optional[Union[int,float]] +# Value must be less than specified or equal (int or float) +# value_in: Optional[Tuple[Union[str,int,float,bool]]] +# Value must match any in the tuple (this is a logical OR statement) +# value_not_in: Optional[Tuple[Union[str,int,float,bool]]] +# Value must not match any in the tuple +# value_in_range: Optional[Tuple[Union[int,float]]] +# Must be a numeric 2-tuple with min and max (inclusive) values - for annotationdata in store.find_data(set="some-set", key="structuretype", value="word"): - # only returns one - ... - Query for all data for a key:: +# Examples +# ------------- - for annotationdata in store.find_data(set="some-set", key="structuretype"): - ... +# Query for specific annotation data:: - Note, the latter can be accomplished more efficiently as:: +# for annotationdata in store.find_data(set="some-set", key="structuretype", value="word"): +# # only returns one +# ... - for annotationdata in store.dataset("some-set").key("structuretype").data(): - ... +# Query for all data for a key:: - `find_data` should be considered as a convenience/shortcut method. - """ +# for annotationdata in store.find_data(set="some-set", key="structuretype"): +# ... + +# Note, the latter can be accomplished more efficiently as:: + +# for annotationdata in store.dataset("some-set").key("structuretype").data(): +# ... + +# `find_data` should be considered as a convenience/shortcut method. +# """ class Annotation: """ `Annotation` represents a particular *instance of annotation* and is the central - concept of the model. They can be considered the primary nodes of the graph model. The + concept of the model. Annotations can be considered the primary nodes of the graph model. The instance of annotation is strictly decoupled from the *data* or key/value of the annotation (:class:`AnnotationData`). After all, multiple instances can be annotated with the same label (multiple annotations may share the same annotation data). @@ -309,33 +357,36 @@ class Annotation: Text selections will be returned in textual order, except if a DirectionalSelector was used. - Text selections may be filtered using the following keyword arguments: + Text selections may be filtered using the following positionl and/or keyword arguments: + + Positional Arguments + ------------------- + + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns text selections referenced by annotations with data matching this key + * :class:`AnnotationData` - Returns text selections referenced by annotations that have this exact data + * :class:`Annotations` or a tuple/list of :class:`Annotation`- Returns text selections referenced by any annotations that are already in the provided :obj:`Annotations` collection (intersection) + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only textselections referenced by annotations with data that is in the provided collection. + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` (see keyword arguments below) Keyword Arguments ------------------- limit: Optional[int] = None The maximum number of results to return (default: unlimited) - filter: Union[AnnotationData,Tuple[AnnotationData],List[AnnotationData],DataKey,Annotations,Data,TextSelectionOperator] - If you want to add multiple different filters, use `filters` instead. - Filter annotations based on: - - * :class:`AnnotationData` - Returns only text selections referenced by annotations that have this exact data (you can only pass this once). - * a tuple/list of :class:`AnnotationData` - Returns only text selections referenced by annotations with data that matches one of the items in the tuple/list. - * :class:`DataKey` - Returns text selections referenced by annotations with data matching this key (you can only pass this once). - * a tuple/list of :class:`DataKey` - * :class:`Annotations` - Returns only text selections referenced by annotations that are already in the provided :obj:`Annotations` collection (intersection) - * :class:`Data` - Returns only text selections referenced by annotations with data that is in the provided :obj:`Data` collection. - filters: List[Union[AnnotationData,DataKey,Annotations,Data]] value: Optional[Union[str,int,float,bool]] - Constrain the search to text selections referenced by annotations with data of a certain value. This can only be used with `filter=DataKey`. + Constrain the search to text selections referenced by annotations with data of a certain value. This is usually used together with passing a :obj:`DataKey` as filter in the positional arguments. This holds the exact value to search for, there are other variants of this keyword available, see :meth:`data` for a full list. """ - def annotations_in_targets(self, recursive= False, limit: Optional[int] = None) -> Annotations: + def annotations_in_targets(self, *args, **kwargs) -> Annotations: """Returns annotations (:class:`Annotations` containing :class:`Annotation`) this annotation refers to (i.e. using an *AnnotationSelector*) - The annotations can be filtered using keyword arguments; see :meth:`annotations`. One extra keyword argument is available for this method (see below). + The annotations can be filtered using positional and/or keyword arguments; see :meth:`annotations`. One extra keyword argument is available for this method (see below). Annotations will returned be in textual order unless recursive is set or a DirectionalSelector is involved. @@ -346,50 +397,51 @@ class Annotation: Follow AnnotationSelectors recursively (default False) """ - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that are referring to this annotation (i.e. others using an AnnotationSelector). - The annotations can be filtered using keyword arguments. + The annotations can be filtered using positional and/or keyword arguments. + + Positional Arguments + ------------------- + + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns annotations with data matching this key + * :class:`AnnotationData` - Returns only annotations that have this exact data + * :class:`Annotations` or a tuple/list of :class:`Annotation`- Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only annotations with data that is in the provided collection. + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` or variants (see keyword arguments below) Keyword Arguments ------------------- limit: Optional[int] = None The maximum number of results to return (default: unlimited) - filter: Union[AnnotationData,Tuple[AnnotationData],List[AnnotationData],DataKey,Annotations,Data] - If you want to add multiple different filters, use `filters` instead. - Filter annotations based on: - - * :class:`AnnotationData` - Returns only annotations that have this exact data (you can only pass this once). - * a tuple/list of :class:`AnnotationData` - Returns only annotations with data that matches one of the items in the tuple/list. - * :class:`DataKey` - Returns annotations with data matching this key (you can only pass this once). - * a tuple/list of :class:`DataKey` - * :class:`Annotations` - Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) - * :class:`Data` - Returns only annotations with data that is in the provided :obj:`Data` collection. - filters: List[Union[AnnotationData,DataKey,Annotations,Data]] value: Optional[Union[str,int,float,bool]] - Constrain the search to annotations with data of a certain value. This can only be used with `filter=DataKey`. + Constrain the search to annotations with data of a certain value. This is usually used together with passing a :obj:`DataKey` as filter in the positional arguments. This holds the exact value to search for, there are other variants of this keyword available, see :meth:`data` for a full list. - Example --------- Filter by data key and value:: key = store.dataset("linguistic-set").key("part-of-speech") - for annotation in store.annotations(filter=key, value="noun"): + for annotation in store.annotations(key, value="noun"): ... But if you already have the key, like in the example above, you may just as well do (more efficient):: for annotation in key.annotations(value="noun"): ... - """ - def test_annotations(self, **kwargs) -> bool: + def test_annotations(self, *args, **kwargs) -> bool: """ Tests whwther there are annotations (:class:`Annotations` containing :class:`Annotation`) that are referring to this annotation (i.e. others using an AnnotationSelector). This method is like :meth:`annotations`, but only tests and does not return the annotations, as such it is more performant. @@ -436,7 +488,7 @@ class Annotation: def selector_kind(self) -> SelectorKind: """Returns the type of the selector of this annotation""" - def data(self, **kwargs) -> Data: + def data(self, *args, **kwargs) -> Data: """ Returns annotation data (:class:`Data` containing :class:`AnnotationData`) used by this annotation. @@ -444,22 +496,26 @@ class Annotation: the data, then just iterating over the annotation directly (:meth:`__iter__`) will be more efficient. Do note that implementing any filtering yourself in Python is much less performant than letting this data method do it for you. + Positional Arguments + ------------------------- + + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns data matching this key + * :class:`Annotation` - Returns data referenced by the mentioned annotation + * :class:`AnnotationData` - Returns only this exact data. Not very useful, use :meth:`test_data` instead. + * :class:`Annotations` or a tuple/list of :class:`Annotation` - Returns data references by annotations in the provided collection. + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only data that is in the provided :obj:`Data` collection (intersection) + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` or variants (see keyword arguments below) + Keyword Arguments ------------------- limit: Optional[int] = None The maximum number of results to return (default: unlimited) - filter: Union[AnnotationData,Tuple[AnnotationData],List[AnnotationData],DataKey,Annotations,Data] - If you want to add multiple different filters, use `filters` instead. - Filter annotations based on: - - * :class:`AnnotationData` - Returns only this exact data (you can only pass this once). Use :meth:`test_data` instead. - * a tuple/list of :class:`AnnotationData` - Returns only annotations with data that matches one of the items in the tuple/list. - * :class:`DataKey` - Returns data matching this key (you can only pass this once). - * a tuple/list of :class:`DataKey` - * :class:`Annotations` - Returns data that is used by by annotations in the provided :obj:`Annotations` collection. - * :class:`Data` - Returns only data that is in the provided :obj:`Data` collection. - filters: List[Union[AnnotationData,DataKey,Annotations,Data]] value: Optional[Union[str,int,float,bool,List[Union[str,int,float,bool]]]] Search for data matching a specific value. This holds exact value to search for. Further variants of this keyword are listed below: @@ -492,20 +548,20 @@ class Annotation: """ - def test_data(self, **kwargs) -> bool: + def test_data(self, *args, **kwargs) -> bool: """ Tests whether certain annotation data is used by this annotation. - The data can be filtered using keyword arguments. See :meth:`data`. + The data can be filtered using positional and/or keyword arguments. See :meth:`data`. Unlike :meth:`data`, this method merely tests without returning the data, and as such is more performant. """ - def related_text(self, operator: TextSelectionOperator, **kwargs) -> TextSelections: + def related_text(self, operator: TextSelectionOperator, *args, **kwargs) -> TextSelections: """ Applies a :class:`TextSelectionOperator` to find all other text selections who are in a specific relation with the ones from the current annotation. Returns a collection :class:`TextSelections` containing all matching :class:`TextSelection` instances. - Text selections will be returned in textual order. They may be filtered via keyword arguments. See :meth:`Annotation.textselections`. + Text selections will be returned in textual order. They may be filtered via positional and/or keyword arguments. See :meth:`Annotation.textselections`. If you are interested in the annotations associated with the found text selections, then add `.annotations()` to the result. @@ -571,26 +627,26 @@ class Annotations: def is_sorted(self) -> bool: """Returns a boolean indicating whether the annotations in this collection are sorted chronologically (earlier annotations before later once). Note that this is distinct from any textual ordering.""" - def data(self, **kwargs) -> Data: + def data(self, *args, **kwargs) -> Data: """ Returns annotation data (:class:`Data` containing :class:`AnnotationData`) used by annotations in this collection. - The data can be filtered using keyword arguments; see :meth:`Annotation.data`. + The data can be filtered using positional and/or keyword arguments; see :meth:`Annotation.data`. If no filters are set (default), all data from all annotations are returned (without duplicates). """ - def test_data(self, **kwargs) -> bool: + def test_data(self, *args, **kwargs) -> bool: """ Tests whether certain annotation data is used by any annotation in this collection. The data can be filtered using keyword arguments. See :meth:`data`. Unlike :meth:`data`, this method merely tests without returning the data, and as such is more performant. """ - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that reference annotations in the current collection (e.g. annotations that target of the current any annotations using an AnnotationSelector). - The annotations can be filtered using keyword arguments; see :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments; see :meth:`Annotation.annotations`. If no filters are set (default), all annotations are returned (without duplicates) in chronological order. Example @@ -604,11 +660,11 @@ class Annotations: ... """ - def annotations_in_targets(self, **kwargs) -> Annotations: + def annotations_in_targets(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that are being referenced by annotations in the current collection (e.g. annotations we target using an AnnotationSelector). - The annotations can be filtered using keyword arguments; see :meth:`Annotation.annotations`. One extra keyword argument is available and explained below. + The annotations can be filtered using positional and/or keyword arguments; see :meth:`Annotation.annotations`. One extra keyword argument is available and explained below. If no filters are set (default), all annotations are returned (without duplicates). Annotations are returned in chronological order. @@ -619,6 +675,20 @@ class Annotations: Follow AnnotationSelectors recursively (default False) """ + def test_annotation(self, *args, **kwargs) -> bool: + """ + Tests whether certain annotations reference any annotation in this collection. + The annotation can be filtered using positional and/or keyword arguments. See :meth:`annotations`. + Unlike :meth:`annotations`, this method merely tests without returning the data, and as such is more performant. + """ + + def test_annotations_in_targets(self, *args, **kwargs) -> Annotations: + """ + Tests whether annotations in this collection targets the specified annotation. + The annotation can be filtered using positional and/or keyword arguments. See :meth:`annotations`. + Unlike :meth:`annotations_in_targets`, this method merely tests without returning the data, and as such is more performant. + """ + def textselections(self, limit: Optional[int] = None) -> TextSelections: """ Returns a collection of all textselections associated with the annotations in this collection. @@ -686,18 +756,18 @@ class AnnotationDataSet: def __iter__(self) -> Iterator[AnnotationData]: """Returns an iterator over all :class:`AnnotationData` in the dataset. If you want to do any filtering, use :meth:`data` instead.""" - def data(self, **kwargs) -> Data: + def data(self, *args, **kwargs) -> Data: """ Returns annotation data (:class:`Data` containing :class:`AnnotationData`) used by this key. - The data can be filtered using keyword arguments. See :meth:`Annotation.data`. + The data can be filtered using positional and/or keyword arguments. See :meth:`Annotation.data`. If you don't intend to do any filtering at all, then just using :meth:`__iter__` may be faster. """ - def test_data(self, **kwargs) -> bool: + def test_data(self, *args, **kwargs) -> bool: """ Tests whether certain annotation data exists in this set. - The data can be filtered using keyword arguments. See :meth:`Annotation.data`. + The data can be filtered using positional and/or keyword arguments. See :meth:`Annotation.data`. This method is like :meth:`data`, but merely tests without returning the data, and as such is more performant. """ @@ -722,11 +792,11 @@ class DataKey: def dataset(self) -> AnnotationDataSet: """Returns the :class:`AnnotationDataSet` this key is part of""" - def data(self, **kwargs) -> Data: + def data(self, *args, **kwargs) -> Data: """ Returns annotation data (:class:`Data` containing :class:`AnnotationData`) used by this key. - The data can be filtered using keyword arguments. See :meth:`Annotation.data`. Note that only a subset makes sense in this context, set and key are already fixed. + The data can be filtered using positional and/or keyword arguments. See :meth:`Annotation.data`. Note that only a subset makes sense in this context, set and key are already fixed. Example -------- @@ -737,7 +807,7 @@ class DataKey: # returns only one """ - def test_data(self, **kwargs) -> bool: + def test_data(self, *args, **kwargs) -> bool: """ Tests whether certain annotation data exists for this key The data can be filtered using keyword arguments. See :meth:`Annotation.data`. Note that only a subset makes sense in this context, set and key are already fixed. @@ -754,7 +824,7 @@ class DataKey: ... """ - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that make use of this key. @@ -769,7 +839,7 @@ class DataKey: ... """ - def test_annotations(self, **kwargs) -> bool: + def test_annotations(self, *args, **kwargs) -> bool: """ Tests whether there are any annotations that make use of this key. This method is like :meth:`annotations`, but only tests and does not return the annotations, as such it is more performant. @@ -797,6 +867,9 @@ class DataKey: The maximum number of results to return (default: unlimited) """ + def select(self) -> Selector: + """Returns a selector pointing to this key (DataKeySelector)""" + class DataValue: """Encapsulates a value and its type. Held by :class:`AnnotationData`. This type is not a reference but holds the actual value.""" @@ -847,33 +920,37 @@ class AnnotationData: def dataset(self) -> AnnotationDataSet: """Returns the :class:`AnnotationDataSet` this data is part of""" - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that make use of this data. - The annotations can be filtered using keyword arguments. + The annotations can be filtered using positional and/or keyword arguments. + + Positional Arguments + ------------------- + + Positional arguments can be of the following types: + + * :class:`DataKey` - Returns annotations with data matching this key + * :class:`AnnotationData` - Returns only annotations that have this exact data + * :class:`Annotations` or a tuple/list of :class:`Annotation`- Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) + * :class:`Data` a tuple/list of :class:`AnnotationData `- Returns only annotations with data that is in the provided collection. + * a dictionary: + * `set` - An ID of an dataset (or a :class:`DataAnnotationSet` instance), only needed when specifying `key` as a string (see below) + * `key` - An key, either an instance of :class:`DataKey` or a string, in the latter case you need to specify `set` as well. + * `value` (see keyword arguments below) Keyword Arguments ------------------- limit: Optional[int] = None The maximum number of results to return (default: unlimited) - filter: Union[AnnotationData,Tuple[AnnotationData],List[AnnotationData],DataKey,Annotations,Data] - If you want to add multiple different filters, use `filters` instead. - Filter annotations based on: - - * :class:`AnnotationData` - Returns only annotations that have this exact data (you can only pass this once). - * a tuple/list of :class:`AnnotationData` - Returns only annotations with data that matches one of the items in the tuple/list. - * :class:`DataKey` - Returns annotations with data matching this key (you can only pass this once). - * :class:`Annotations` - Returns only annotations that are already in the provided :obj:`Annotations` collection (intersection) - * :class:`Data` - Returns only annotations with data that is in the provided :obj:`Data` collection. - filters: List[Union[AnnotationData,DataKey,Annotations,Data]] value: Optional[Union[str,int,float,bool]] - Constrain the search to annotations with data of a certain value. This can only be used with `filter=DataKey`. + Constrain the search to annotations with data of a certain value. This holds the exact value to search for, there are other variants of this keyword available, see :meth:`data` for a full list. """ - def test_annotations(self, **kwargs) -> bool: + def test_annotations(self, *args, **kwargs) -> bool: """ Tests whether there are any annotations that make use of this data. This method is like :meth:`annotations`, but only tests and does not return the annotations, as such it is more performant. @@ -892,6 +969,9 @@ class AnnotationData: The maximum number of results to return (default: unlimited) """ + def select(self) -> Selector: + """Returns a selector pointing to this data (AnnotationDataSelector)""" + class Data: """ A `Data` object holds an arbitrary collection of annotation data. @@ -907,19 +987,19 @@ class Data: def __getitem__(self, int) -> AnnotationData: """Returns data in the collection by index""" - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that are make use of any of the data in this collection - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def test_annotations(self, **kwargs) -> bool: + def test_annotations(self, *args, **kwargs) -> bool: """ Tests whether there are any annotations that make use of any of the data in this collection This method is like :meth:`annotations`, but does only tests and does not return the annotations, as such it is more performant. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ class TextSelections: @@ -949,11 +1029,11 @@ class TextSelections: def text(self, delimiter: str) -> List[str]: """Returns the text of all textselections in a list""" - def annotations(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """ Returns annotations (:class:`Annotations` containing :class:`Annotation`) that refer to any of the text selections in this collection - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ def test_annotations(self, **kwargs) -> bool: @@ -962,24 +1042,32 @@ class TextSelections: This method is like :meth:`annotations`, but only tests and does not return the annotations, as such it is more performant. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def test_data(self, **kwargs) -> bool: + def data(self, *args, **kwargs) -> Data: + """ + Returns annotation data (:class:`Data` containing :class:`AnnotationData`) used by annotations referring to the text selections in this collection. + + The data can be filtered using positional and/or keyword arguments; see :meth:`Annotation.data`. + If no filters are set (default), all data from all annotations on all text selections are returned (without duplicates). + """ + + def test_data(self, *args, **kwargs) -> bool: """ Tests whether there are any annotations that reference any of the text selections in the iterator, with data that passes the provided filters. The result is functionally equivalent to doing `.annotations().test_data()`, but this shortcut method is implemented much more efficiently and therefore recommended. - The data can be filtered using keyword arguments. See :meth:`Annotations.data`. + The data can be filtered using positional and/or keyword arguments. See :meth:`Annotations.data`. """ - def related_text(self, operator: TextSelectionOperator, **kwargs) -> TextSelections: + def related_text(self, operator: TextSelectionOperator, *args, **kwargs) -> TextSelections: """ Applies a :class:`TextSelectionOperator` to find all other text selections who are in a specific relation with the ones from the current collections. Returns a collection of all matching :class:`TextSelection` instances. - Text selections will be returned in textual order. They may be filtered via keyword arguments. See :meth:`Annotation.textselections`. + Text selections will be returned in textual order. They may be filtered via positional and/or keyword arguments. See :meth:`Annotation.textselections`. If you are interested in the annotations associated with the found text selections, then add `.annotations()` to the result. @@ -1169,6 +1257,8 @@ class SelectorKind: ANNOTATIONSELECTOR: SelectorKind TEXTSELECTOR: SelectorKind DATASETSELECTOR: SelectorKind + DATAKEYSELECTOR: SelectorKind + ANNOTATIONDATASELECTOR: SelectorKind MULTISELECTOR: SelectorKind COMPOSITESELECTOR: SelectorKind DIRECTIONALSELECTOR: SelectorKind @@ -1345,73 +1435,59 @@ class TextResource: The parameter value must be 0 or negative. """ - def annotations(self, **kwargs) -> Annotations: - """Returns a collection of annotations (:class:`Annotations` contiaining :class:`Annotation`) that reference this resource via any selector. - - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. - """ - - def annotations_on_text(self, **kwargs) -> Annotations: + def annotations(self, *args, **kwargs) -> Annotations: """Returns a collection of annotations (:class:`Annotation`) that reference this resource via a *TextSelector* (if any). Does *NOT* include those that use a ResourceSelector, use :meth:`annotations_metadata` instead for those instead. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def annotations_as_metadata(self, **kwargs) -> Annotations: + def annotations_as_metadata(self, *args, **kwargs) -> Annotations: """Returns a collection of annotations (:class:`Annotation`) that reference this resource via a *ResourceSelector* (if any). Does *NOT* include those that use a TextSelector, use :meth:`annotations` instead for those instead. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def test_annotations(self, **kwargs) -> bool: + def test_annotations(self,*args, **kwargs) -> bool: """ - Tests whether there are any annotations that reference this resource (via any selector). + Tests whether there are any annotations that reference the text of this resource (via a TextSelector). This method is like :meth:`annotations`, but only tests and does not return the annotations, as such it is more performant. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def test_annotations_as_metadata(self, **kwargs) -> bool: + def test_annotations_as_metadata(self, *args, **kwargs) -> bool: """ Tests whether there are any annotations that reference this resource as metadata (via a ResourceSelector). This method is like :meth:`annotations_as_metadata`, but only tests and does not return the annotations, as such it is more performant. - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. + The annotations can be filtered using positional and/or keyword arguments. See :meth:`Annotation.annotations`. """ - def test_annotations_on_text(self, **kwargs) -> bool: - """ - Tests whether there are any annotations that reference the text of this resource (via a TextSelector). - - This method is like :meth:`annotations_on_text`, but only tests and does not return the annotations, as such it is more performant. - - The annotations can be filtered using keyword arguments. See :meth:`Annotation.annotations`. - """ - def related_text(self, operator: TextSelectionOperator, referenceselections: List[TextSelection], **kwargs) -> TextSelections: - """ - Applies a :class:`TextSelectionOperator` to find all other - text selections who are in a specific relation with the ones from `referenceselections`. - Returns all matching :class:`TextSelection` instances in a collection :class:`TextSelections`. +# def related_text(self, operator: TextSelectionOperator, referenceselections: List[TextSelection], **kwargs) -> TextSelections: +# """ +# Applies a :class:`TextSelectionOperator` to find all other +# text selections who are in a specific relation with the ones from `referenceselections`. +# Returns all matching :class:`TextSelection` instances in a collection :class:`TextSelections`. - Text selections will be returned in textual order. They may be filtered via keyword arguments. See :meth:`Annotation.textselections`. - - Parameters - ------------ +# Text selections will be returned in textual order. They may be filtered via keyword arguments. See :meth:`Annotation.textselections`. +# +# Parameters +# ------------ - operator: TextSelectionOperator - The operator to apply when comparing text selections - referenceselections: List[TextSelection] - Text selections to use as reference - +# operator: TextSelectionOperator +# The operator to apply when comparing text selections +# referenceselections: List[TextSelection] +# Text selections to use as reference +# - See :meth:`Annotation.related_text` for allowed keyword arguments. - """ +# See :meth:`Annotation.related_text` for allowed keyword arguments. +# """ class TextSelection: """ diff --git a/test.py b/test.py index c924aab..a52c252 100644 --- a/test.py +++ b/test.py @@ -235,7 +235,7 @@ def test_annotations_by_data(self): def test_find_data(self): """Find annotationdata by value""" dataset = self.store.dataset("testdataset") - results = dataset.data(filter=dataset.key("pos"), value="noun") + results = dataset.data(dataset.key("pos"), value="noun") self.assertIsInstance(results, Data) self.assertEqual(len(results), 1) self.assertIsInstance(results[0], AnnotationData) @@ -254,7 +254,7 @@ def test_find_data_from_key(self): def test_find_data_missing(self): """Find annotationdata by value, test mismatches""" dataset = self.store.dataset("testdataset") - results = dataset.data(filter=dataset.key("pos"),value="non-existent") + results = dataset.data(dataset.key("pos"),value="non-existent") self.assertFalse(results) #empty evaluates to False