diff --git a/DictDots.py b/DictDots.py index 8254ede..01a4233 100644 --- a/DictDots.py +++ b/DictDots.py @@ -1,13 +1,13 @@ import re import constants from typing import Any -from dd_types import DotSearchable, DotQuery, DotCurrentKey, DotCurrentData +from dd_types import DotSearchable, DotQuery, DotCurrentKey, DotCurrentData, DotFilterResult from dd_exceptions import InvalidQueryString, InvalidDataType, DoesNotExist, KeyNotFound class DictDots: @staticmethod - def is_valid_query(query: DotQuery) -> bool: + def is_valid_get_query(query: DotQuery) -> bool: """Check if the query string has only valid characters. Queries, for the time being, only allow alphanumeric and dots ``.`` @@ -19,6 +19,10 @@ def is_valid_query(query: DotQuery) -> bool: """ return bool(re.match(r'^[\w.]+$', query)) + @classmethod + def is_valid_filter_query(query: DotQuery): + return bool(re.match(r'^[\w.\[\]{}]+$', query)) + @staticmethod def is_searchable_type(data: Any) -> bool: """Check that the data can be searched by DictDots. @@ -44,7 +48,7 @@ def _validate_get(searchable: DotSearchable, query: DotQuery) -> None: if not DictDots.is_searchable_type(searchable): raise InvalidDataType(searchable) - if not DictDots.is_valid_query(query): + if not DictDots.is_valid_get_query(query): raise InvalidQueryString(query) @staticmethod @@ -104,5 +108,50 @@ def get(cls, searchable: DotSearchable, query: DotQuery, default: Any = None) -> return current_data + @classmethod + def filter(cls, searchable: DotSearchable, query: DotQuery) -> DotFilterResult: + """Query a searchable + + Attempt to find values in a searchable matching the key from the search string. + Returns a list of all matches. + Will return an empty list if no matches are found. + + Does not raise an error for invalid keys because this is a search. + + :param DotSearchable searchable: + The data to query. + :param DotQuery query: + A query string to search the data for. + :return DotFilterResult: + A list of all data matching the search string in ``query``. + May be empty. + """ + DictDots._validate_get(searchable, query) + keys = query.split('.') + # current_data is the value we are currently digging into. + current_data = searchable + + type_methods = { + dict: cls._dict_getter, + list: cls._list_getter, + } + + for key in keys: + if key.isnumeric(): + # We don't support numerical strings for now, so convert them to ints. + key = int(key) + + method = type_methods[type(current_data)] + + try: + current_data = method(key, current_data) + except KeyNotFound as e: + if default: + return default + raise DoesNotExist(query, searchable, e) + + return current_data + + diff --git a/dd_types.py b/dd_types.py index 450dabb..a9b99e7 100644 --- a/dd_types.py +++ b/dd_types.py @@ -17,3 +17,6 @@ """A type representing the accepted data types for getter functions.""" DotCurrentData = DotSearchable + +"""The result of a filter query. Can be empty.""" +DotFilterResult = List[Any] diff --git a/docs/ddql.md b/docs/ddql.md index 8713325..fd8bdae 100644 --- a/docs/ddql.md +++ b/docs/ddql.md @@ -1,11 +1,13 @@ # Potential data language +--- This is a wishlist of how I want the query language to look. The only things on here that work right now are `get s` and `get ns`. I haven't yet decided if I actually want to support sets due to how the indexing works through a hash. It would be cumbersome to try and pass a big value into dictdots in the query string. + ## Get Get returns a specific object matching an exact query. diff --git a/tests/test_dict_dots.py b/tests/test_dict_dots.py index bffafa1..d7b171d 100644 --- a/tests/test_dict_dots.py +++ b/tests/test_dict_dots.py @@ -12,7 +12,7 @@ ]) def test_is_valid_query(query, result): """Test that queries can be validated.""" - assert DictDots.is_valid_query(query) == result + assert DictDots.is_valid_get_query(query) == result @pytest.mark.parametrize("data,expected", [