Skip to content

Commit

Permalink
Merge pull request #1563 from opendatacube/not_expression
Browse files Browse the repository at this point in the history
add "Not" operator
  • Loading branch information
Ariana-B committed Mar 14, 2024
2 parents 4ac020f + 0b550ad commit b6cebf5
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 11 deletions.
12 changes: 6 additions & 6 deletions datacube/index/abstract.py
Expand Up @@ -21,7 +21,7 @@
from datacube.config import LocalConfig
from datacube.index.exceptions import TransactionException
from datacube.index.fields import Field
from datacube.model import Dataset, MetadataType, Range
from datacube.model import Dataset, MetadataType, Range, Not
from datacube.model import Product
from datacube.utils import cached_property, jsonify_document, read_documents, InvalidDocException
from datacube.utils.changes import AllowPolicy, Change, Offset, DocumentMismatchError, check_doc_unchanged
Expand Down Expand Up @@ -374,7 +374,7 @@ def get_all_docs(self) -> Iterable[Mapping[str, Any]]:
yield mdt.definition


QueryField = Union[str, float, int, Range, datetime.datetime]
QueryField = Union[str, float, int, Range, datetime.datetime, Not]
QueryDict = Mapping[str, QueryField]


Expand Down Expand Up @@ -688,7 +688,7 @@ def search(self, **query: QueryField) -> Iterator[Product]:
@abstractmethod
def search_robust(self,
**query: QueryField
) -> Iterable[Tuple[Product, Mapping[str, QueryField]]]:
) -> Iterable[Tuple[Product, QueryDict]]:
"""
Return dataset types that match match-able fields and dict of remaining un-matchable fields.
Expand All @@ -698,7 +698,7 @@ def search_robust(self,

@abstractmethod
def search_by_metadata(self,
metadata: Mapping[str, QueryField]
metadata: QueryDict
) -> Iterable[Dataset]:
"""
Perform a search using arbitrary metadata, returning results as Product objects.
Expand Down Expand Up @@ -1115,7 +1115,7 @@ def restore_location(self,

@abstractmethod
def search_by_metadata(self,
metadata: Mapping[str, QueryField]
metadata: QueryDict
) -> Iterable[Dataset]:
"""
Perform a search using arbitrary metadata, returning results as Dataset objects.
Expand All @@ -1129,7 +1129,7 @@ def search_by_metadata(self,
@abstractmethod
def search(self,
limit: Optional[int] = None,
source_filter: Optional[Mapping[str, QueryField]] = None,
source_filter: Optional[QueryDict] = None,
**query: QueryField) -> Iterable[Dataset]:
"""
Perform a search, returning results as Dataset objects.
Expand Down
14 changes: 13 additions & 1 deletion datacube/index/fields.py
Expand Up @@ -10,7 +10,7 @@
from dateutil.tz import tz
from typing import List

from datacube.model import Range
from datacube.model import Range, Not
from datacube.model.fields import Expression, Field

__all__ = ['Field',
Expand All @@ -36,6 +36,16 @@ def evaluate(self, ctx):
return any(expr.evaluate(ctx) for expr in self.exprs)


class NotExpression(Expression):
def __init__(self, expr):
super(NotExpression, self).__init__()
self.expr = expr
self.field = expr.field

def evaluate(self, ctx):
return not self.expr.evaluate(ctx)


def as_expression(field: Field, value) -> Expression:
"""
Convert a single field/value to expression, following the "simple" conventions.
Expand All @@ -44,6 +54,8 @@ def as_expression(field: Field, value) -> Expression:
return field.between(value.begin, value.end)
elif isinstance(value, list):
return OrExpression(*(as_expression(field, val) for val in value))
elif isinstance(value, Not):
return NotExpression(as_expression(field, value.value))
# Treat a date (day) as a time range.
elif isinstance(value, date) and not isinstance(value, datetime):
return as_expression(
Expand Down
2 changes: 1 addition & 1 deletion datacube/model/__init__.py
Expand Up @@ -21,7 +21,7 @@
schema_validated, DocReader
from datacube.index.eo3 import is_doc_eo3
from .fields import Field, get_dataset_fields
from ._base import Range, ranges_overlap # noqa: F401
from ._base import Range, ranges_overlap, Not # noqa: F401
from .eo3 import validate_eo3_compatible_type

from deprecat import deprecat
Expand Down
3 changes: 3 additions & 0 deletions datacube/model/_base.py
Expand Up @@ -18,3 +18,6 @@ def ranges_overlap(ra: Range, rb: Range) -> bool:
if ra.begin <= rb.begin:
return ra.end > rb.begin
return rb.end > ra.begin


Not = namedtuple('Not', 'value')
1 change: 1 addition & 0 deletions docs/about/whats_new.rst
Expand Up @@ -16,6 +16,7 @@ v1.8.next
- Tweak ``list_products`` logic for getting crs and resolution values (:pull:`1535`)
- Add new ODC Cheatsheet reference doc to Data Access & Analysis documentation page (:pull:`1543`)
- Fix broken codecov github action. (:pull:`1554`)
- Add generic NOT operator and for ODC queries and ``Not`` type wrapper (:pull:`1563`)

v1.8.17 (8th November 2023)
===========================
Expand Down
12 changes: 9 additions & 3 deletions integration_tests/index/test_config_docs.py
Expand Up @@ -15,7 +15,7 @@
from datacube.index import Index
from datacube.index.abstract import default_metadata_type_docs
from datacube.model import MetadataType, DatasetType
from datacube.model import Range, Dataset
from datacube.model import Range, Not, Dataset
from datacube.utils import changes
from datacube.utils.documents import documents_equal
from datacube.testutils import sanitise_doc
Expand Down Expand Up @@ -447,7 +447,7 @@ def test_filter_types_by_fields(index, wo_eo3_product):
assert len(res) == 0


def test_filter_types_by_search(index, wo_eo3_product):
def test_filter_types_by_search(index, wo_eo3_product, ls8_eo3_product):
"""
:type ls5_telem_type: datacube.model.DatasetType
:type index: datacube.index.Index
Expand All @@ -456,7 +456,7 @@ def test_filter_types_by_search(index, wo_eo3_product):

# No arguments, return all.
res = list(index.products.search())
assert res == [wo_eo3_product]
assert res == [ls8_eo3_product, wo_eo3_product]

# Matching fields
res = list(index.products.search(
Expand Down Expand Up @@ -491,6 +491,12 @@ def test_filter_types_by_search(index, wo_eo3_product):
))
assert res == [wo_eo3_product]

# Not expression test
res = list(index.products.search(
product_family=Not("wo"),
))
assert res == [ls8_eo3_product]

# Mismatching fields
res = list(index.products.search(
product_family='spam',
Expand Down

0 comments on commit b6cebf5

Please sign in to comment.