diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 745d3adf..3d27ae11 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,11 @@ Thanks for interested to contribute to TerminusDB Client, to get started, fork t Make sure you have Python>=3.9 installed. We use [pipenv](https://pipenv-fork.readthedocs.io/en/latest/) for dev environment, to install pipenv: -`pip3 install pipenv --upgrade` +``` +python3 -m venv venv +source venv/bin/activate +pip3 install pipenv --upgrade +``` [Fork and clone](https://help.github.com/en/github/getting-started-with-github/fork-a-repo) this repo, then in your local repo: diff --git a/terminusdb_client/tests/test_slice.py b/terminusdb_client/tests/test_slice.py new file mode 100644 index 00000000..38e76075 --- /dev/null +++ b/terminusdb_client/tests/test_slice.py @@ -0,0 +1,75 @@ +""" +Unit tests for WOQL slice operator + +Tests the Python client binding for slice(input_list, result, start, end=None) +""" + +from terminusdb_client.woqlquery.woql_query import WOQLQuery + +from .woqljson.woqlSliceJson import WOQL_SLICE_JSON + + +class TestWOQLSlice: + """Test cases for the slice operator""" + + def test_basic_slice(self): + """Basic slicing - slice([a,b,c,d], result, 1, 3) returns [b,c]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) + assert woql_object.to_dict() == WOQL_SLICE_JSON["basicSlice"] + + def test_negative_indices(self): + """Negative indices - slice([a,b,c,d], result, -2, -1) returns [c]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2, -1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["negativeIndices"] + + def test_without_end(self): + """Optional end - slice([a,b,c,d], result, 1) returns [b,c,d]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["withoutEnd"] + + def test_variable_list(self): + """Variable list input - slice(v:MyList, result, 0, 2)""" + woql_object = WOQLQuery().slice("v:MyList", "v:Result", 0, 2) + assert woql_object.to_dict() == WOQL_SLICE_JSON["variableList"] + + def test_variable_indices(self): + """Variable indices - slice([x,y,z], result, v:Start, v:End)""" + woql_object = WOQLQuery().slice(["x", "y", "z"], "v:Result", "v:Start", "v:End") + assert woql_object.to_dict() == WOQL_SLICE_JSON["variableIndices"] + + def test_empty_list(self): + """Empty list - slice([], result, 0, 1) returns []""" + woql_object = WOQLQuery().slice([], "v:Result", 0, 1) + assert woql_object.to_dict() == WOQL_SLICE_JSON["emptyList"] + + def test_slice_type(self): + """Verify the @type is correctly set to 'Slice'""" + woql_object = WOQLQuery().slice(["a", "b"], "v:Result", 0, 1) + assert woql_object.to_dict()["@type"] == "Slice" + + def test_chaining_with_and(self): + """Test that slice works with method chaining via woql_and""" + woql_object = WOQLQuery().woql_and( + WOQLQuery().eq("v:MyList", ["a", "b", "c"]), + WOQLQuery().slice("v:MyList", "v:Result", 1, 3), + ) + result = woql_object.to_dict() + assert result["@type"] == "And" + assert len(result["and"]) == 2 + assert result["and"][1]["@type"] == "Slice" + + def test_single_element_slice(self): + """Single element - slice([a,b,c,d], result, 1, 2) returns [b]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 2) + result = woql_object.to_dict() + assert result["@type"] == "Slice" + assert result["start"]["data"]["@value"] == 1 + assert result["end"]["data"]["@value"] == 2 + + def test_full_range(self): + """Full range - slice([a,b,c,d], result, 0, 4) returns [a,b,c,d]""" + woql_object = WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 0, 4) + result = woql_object.to_dict() + assert result["@type"] == "Slice" + assert result["start"]["data"]["@value"] == 0 + assert result["end"]["data"]["@value"] == 4 diff --git a/terminusdb_client/tests/woqljson/woqlSliceJson.py b/terminusdb_client/tests/woqljson/woqlSliceJson.py new file mode 100644 index 00000000..4183bb59 --- /dev/null +++ b/terminusdb_client/tests/woqljson/woqlSliceJson.py @@ -0,0 +1,77 @@ +"""Expected JSON output for WOQL slice operator tests""" + +WOQL_SLICE_JSON = { + "basicSlice": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 3}}, + }, + "negativeIndices": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -2}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": -1}}, + }, + "withoutEnd": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "a"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "b"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "c"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "d"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + # Note: no 'end' property when end is omitted + }, + "variableList": { + "@type": "Slice", + "list": {"@type": "DataValue", "variable": "MyList"}, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 2}}, + }, + "variableIndices": { + "@type": "Slice", + "list": { + "@type": "DataValue", + "list": [ + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "x"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "y"}}, + {"@type": "DataValue", "data": {"@type": "xsd:string", "@value": "z"}}, + ], + }, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "variable": "Start"}, + "end": {"@type": "DataValue", "variable": "End"}, + }, + "emptyList": { + "@type": "Slice", + "list": {"@type": "DataValue", "list": []}, + "result": {"@type": "DataValue", "variable": "Result"}, + "start": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 0}}, + "end": {"@type": "DataValue", "data": {"@type": "xsd:integer", "@value": 1}}, + }, +} diff --git a/terminusdb_client/woqlquery/woql_query.py b/terminusdb_client/woqlquery/woql_query.py index 35fbad21..2e930c82 100644 --- a/terminusdb_client/woqlquery/woql_query.py +++ b/terminusdb_client/woqlquery/woql_query.py @@ -2492,6 +2492,54 @@ def sum(self, user_input, output): self._cursor["sum"] = self._clean_data_value(output) return self + def slice(self, input_list, result, start, end=None): + """ + Extracts a contiguous subsequence from a list, following slice() semantics. + + Parameters + ---------- + input_list : list or str + A list of values or a variable representing a list + result : str + A variable that stores the sliced result + start : int or str + The start index (0-based, supports negative indices) + end : int or str, optional + The end index (exclusive). If omitted, takes the rest of the list + + Returns + ------- + WOQLQuery object + query object that can be chained and/or execute + + Examples + -------- + >>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", 1, 3) # ["b", "c"] + >>> WOQLQuery().slice(["a", "b", "c", "d"], "v:Result", -2) # ["c", "d"] + """ + if input_list and input_list == "args": + return ["list", "result", "start", "end"] + if self._cursor.get("@type"): + self._wrap_cursor_with_and() + self._cursor["@type"] = "Slice" + self._cursor["list"] = self._data_list(input_list) + self._cursor["result"] = self._clean_data_value(result) + if isinstance(start, int): + self._cursor["start"] = self._clean_data_value( + {"@type": "xsd:integer", "@value": start} + ) + else: + self._cursor["start"] = self._clean_data_value(start) + # end is optional + if end is not None: + if isinstance(end, int): + self._cursor["end"] = self._clean_data_value( + {"@type": "xsd:integer", "@value": end} + ) + else: + self._cursor["end"] = self._clean_data_value(end) + return self + def start(self, start, query=None): """Specifies that the start of the query returned