diff --git a/README.md b/README.md index 0d1bf40..5d8f384 100644 --- a/README.md +++ b/README.md @@ -66,6 +66,7 @@ of JanusGraph-Python: | JanusGraph-Python | JanusGraph | | ----------------- | ---------------------- | | 1.0.z | 1.0.z | +| 1.1.z | (1.0.z,) 1.1.z | While it should also be possible to use JanusGraph-Python with other versions of JanusGraph than mentioned here, compatibility is not tested and some diff --git a/janusgraph_python/process/traversal.py b/janusgraph_python/process/traversal.py index 5c918a1..f624a83 100644 --- a/janusgraph_python/process/traversal.py +++ b/janusgraph_python/process/traversal.py @@ -46,6 +46,16 @@ def text_contains(*args): """ return _JanusGraphP("textContains", *args) + @staticmethod + def text_not_contains(*args): + """ + Is true if no words inside the text string match the query string. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotContains", *args) + @staticmethod def text_contains_prefix(*args): """ @@ -56,6 +66,16 @@ def text_contains_prefix(*args): """ return _JanusGraphP("textContainsPrefix", *args) + @staticmethod + def text_not_contains_prefix(*args): + """ + Is true if no words inside the text string begin with the query string. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotContainsPrefix", *args) + @staticmethod def text_contains_regex(*args): """ @@ -66,6 +86,16 @@ def text_contains_regex(*args): """ return _JanusGraphP("textContainsRegex", *args) + @staticmethod + def text_not_contains_regex(*args): + """ + Is true if no words inside the text string match the given regular expression. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotContainsRegex", *args) + @staticmethod def text_contains_fuzzy(*args): """ @@ -77,6 +107,37 @@ def text_contains_fuzzy(*args): """ return _JanusGraphP("textContainsFuzzy", *args) + @staticmethod + def text_not_contains_fuzzy(*args): + """ + Is true if no words inside the text string are similar to the query string + (based on Levenshtein edit distance). + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotContainsFuzzy", *args) + + @staticmethod + def text_contains_phrase(*args): + """ + Is true if the text string does contain the sequence of words in the query string. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textContainsPhrase", *args) + + @staticmethod + def text_not_contains_phrase(*args): + """ + Is true if the text string does not contain the sequence of words in the query string. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotContainsPhrase", *args) + @staticmethod def text_prefix(*args): """ @@ -87,6 +148,16 @@ def text_prefix(*args): """ return _JanusGraphP("textPrefix", *args) + @staticmethod + def text_not_prefix(*args): + """ + Is true if the string value does not start with the given query string. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotPrefix", *args) + @staticmethod def text_regex(*args): """ @@ -97,6 +168,16 @@ def text_regex(*args): """ return _JanusGraphP("textRegex", *args) + @staticmethod + def text_not_regex(*args): + """ + Is true if the string value does not match the given regular expression in its entirety. + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotRegex", *args) + @staticmethod def text_fuzzy(*args): """ @@ -107,6 +188,16 @@ def text_fuzzy(*args): """ return _JanusGraphP("textFuzzy", *args) + @staticmethod + def text_not_fuzzy(*args): + """ + Is true if the string value is not similar to the given query string (based on Levenshtein edit distance). + + :param query: string, The query to search. + :return The text predicate. + """ + return _JanusGraphP("textNotFuzzy", *args) + class RelationIdentifier(object): _TO_STRING_DELIMITER = '-' diff --git a/requirements.txt b/requirements.txt index 645ec78..b8c5be1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1 @@ -aiohttp==3.10.10 - -# on Python 3.11 and 3.12 this is not installed automatically as dependency of aiohttp -async-timeout==4.0.3 - -gremlinpython==3.7.2 +gremlinpython==3.7.3 diff --git a/setup.py b/setup.py index 5dffede..408cdf8 100644 --- a/setup.py +++ b/setup.py @@ -18,7 +18,7 @@ def read_requirements(path): setup( name='janusgraphpython', - version='1.0.0', + version='1.1.0', description='JanusGraph-Python extends Apache TinkerPop™''s Gremlin-Python with support for JanusGraph-specific types.', long_description=(this_directory/'README.md').read_text(), long_description_content_type='text/markdown', diff --git a/tests/integration/Text_test.py b/tests/integration/Text_test.py index 381790c..b09c63a 100644 --- a/tests/integration/Text_test.py +++ b/tests/integration/Text_test.py @@ -30,6 +30,18 @@ def test_text_contains_given_search_text(self, search_text, expected_count): count = self.g.E().has('reason', Text.text_contains(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('loves', 1), + param('shouldNotBeFound', 3), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_contains_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_not_contains(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -42,6 +54,19 @@ def test_text_contains_prefix_given_search_text(self, search_text, expected_coun count = self.g.E().has('reason', Text.text_contains_prefix(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('wave', 2), + param('f', 1), + param('shouldNotBeFound', 3), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_contains_not_prefix_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_not_contains_prefix(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -54,6 +79,19 @@ def test_text_contains_regex_given_search_text(self, search_text, expected_count count = self.g.E().has('reason', Text.text_contains_regex(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('.*ave.*', 2), + param('f.{3,4}', 1), + param('shouldNotBeFound', 3), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_contains_regex_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_not_contains_regex(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -65,6 +103,46 @@ def test_text_contains_fuzzy_given_search_text(self, search_text, expected_count count = self.g.E().has('reason', Text.text_contains_fuzzy(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('waxes', 2), + param('shouldNotBeFound', 3), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_contains_fuzzy_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_not_contains_fuzzy(search_text)).count().next() + assert count == expected_count + + @mark.parametrize( + 'search_text,expected_count', + [ + param('fresh breezes', 1), + param('no fear', 1), + param('fear of', 1), + param('should not be found', 0), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_contains_phrase_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_contains_phrase(search_text)).count().next() + assert count == expected_count + + @mark.parametrize( + 'search_text,expected_count', + [ + param('fresh breezes', 2), + param('no fear', 2), + param('fear of', 2), + param('should not be found', 3), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_contains_phrase_given_search_text(self, search_text, expected_count): + count = self.g.E().has('reason', Text.text_not_contains_phrase(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -77,6 +155,19 @@ def test_text_prefix_given_search_text(self, search_text, expected_count): count = self.g.V().has('name', Text.text_prefix(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('herc', 11), + param('s', 9), + param('shouldNotBeFound', 12), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_prefix_given_search_text(self, search_text, expected_count): + count = self.g.V().has('name', Text.text_not_prefix(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -89,6 +180,19 @@ def test_text_regex_given_search_text(self, search_text, expected_count): count = self.g.V().has('name', Text.text_regex(search_text)).count().next() assert count == expected_count + @mark.parametrize( + 'search_text,expected_count', + [ + param('.*rcule.*', 11), + param('s.{2}', 10), + param('shouldNotBeFound', 12), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_regex_given_search_text(self, search_text, expected_count): + count = self.g.V().has('name', Text.text_not_regex(search_text)).count().next() + assert count == expected_count + @mark.parametrize( 'search_text,expected_count', [ @@ -100,4 +204,16 @@ def test_text_regex_given_search_text(self, search_text, expected_count): def test_text_fuzzy_given_search_text(self, search_text, expected_count): count = self.g.V().has('name', Text.text_fuzzy(search_text)).count().next() assert count == expected_count - \ No newline at end of file + + @mark.parametrize( + 'search_text,expected_count', + [ + param('herculex', 11), + param('ska', 10), + param('shouldNotBeFound', 12), + ] + ) + @mark.minimum_janusgraph_version("1.1.0") + def test_text_not_fuzzy_given_search_text(self, search_text, expected_count): + count = self.g.V().has('name', Text.text_not_fuzzy(search_text)).count().next() + assert count == expected_count \ No newline at end of file diff --git a/tests/integration/config.ini b/tests/integration/config.ini deleted file mode 100644 index a9d4717..0000000 --- a/tests/integration/config.ini +++ /dev/null @@ -1,2 +0,0 @@ -[docker] -image = janusgraph/janusgraph:1.0.0 diff --git a/tests/integration/config.json b/tests/integration/config.json new file mode 100644 index 0000000..a620779 --- /dev/null +++ b/tests/integration/config.json @@ -0,0 +1,4 @@ +{ + "dockerRepository": "janusgraph/janusgraph", + "dockerTags": ["1.0.1", "1.1.0"] +} \ No newline at end of file diff --git a/tests/integration/conftest.py b/tests/integration/conftest.py index 239a662..06c202c 100644 --- a/tests/integration/conftest.py +++ b/tests/integration/conftest.py @@ -13,23 +13,79 @@ # limitations under the License. import os +import json import time import pathlib import configparser -from pytest import fixture +from pytest import fixture, param, skip, exit from testcontainers.core.container import DockerContainer from gremlin_python.process.anonymous_traversal import traversal from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection from janusgraph_python.driver.serializer import JanusGraphSONSerializersV3d0 +current_path = pathlib.Path(__file__).parent.resolve() +JANUSGRAPH_REPOSITORY = None +JANUSGRAPH_VERSION_PARAMS = [] + +# read integration tests config from JSON +with open(os.path.join(current_path, "config.json")) as f: + config = json.load(f) + JANUSGRAPH_REPOSITORY = config.get("dockerRepository", None) + docker_tags = config.get("dockerTags", []) + JANUSGRAPH_VERSION_PARAMS = [param(t, id=t) for t in docker_tags] + +def pytest_configure(config): + # registering custom marker to be able to skip integration tests + # that are incompatible with older JanusGraph version + config.addinivalue_line( + "markers", "minimum_janusgraph_version(version): mark integration test with minimum required JanusGraph version" + ) + +# this fixture should be used for all test methods in the integration tests +@fixture(autouse=True, scope='function') +def janusgraph_compatibility(request): + """ + Fixture to check if a given integration test is allowed to run on a given + JanusGraph version. If the version is not satisfied, the test will be skipped. + """ + # get minimum desired JanusGraph version for the test if defined + marker = request.node.get_closest_marker("minimum_janusgraph_version") + # if no minimum desired JanusGraph version is defined, no need to check compatibility + if not marker or not marker.args: + return + + min_jg_version = marker.args[0] + + # get version of JanusGraph used by the current test run + if len(request.fixturenames) == 0: + exit("Fixtures are not used on the expected way") + + top_fixture = request.fixturenames[0] + current_jg_version = request.node.callspec.params.get(top_fixture) + if not current_jg_version: + exit(f"{top_fixture} fixture needs to be parametrized with the list of JanusGraph versions to test with") + + if current_jg_version == min_jg_version: + return + + jg_v_list = [int(num) for num in current_jg_version.split('.')] + min_v_list = [int(num) for num in min_jg_version.split('.')] + + for jg_v, min_v in zip(jg_v_list, min_v_list): + if jg_v < min_v: + return skip(f"not compatible with JanusGraph {current_jg_version}") + @fixture(scope='session') def graph_connection_graphson(request, graph_container): """ Fixture for creating connection with JanusGraphSONSerializersV3d0 serializer to the JanusGraph container """ - return graph_connection(request, graph_container, JanusGraphSONSerializersV3d0()) + # NOTE: this is a workaround to be able to pass the session fixture param + # to the graph_container fixture + container = graph_container(request) + return graph_connection(request, container, JanusGraphSONSerializersV3d0()) def graph_connection(request, graph_container, serializer): """ @@ -58,49 +114,55 @@ def graph_container(request): Fixture for creating JanusGraph container before first test and dropping container after last test in the test session """ - container = None - current_path = pathlib.Path(__file__).parent.resolve() - - def is_server_ready(): - """ - Method to test if JanusGraph server is up and running and filled with test data - """ - connection = None - - while True: - try: - connection = DriverRemoteConnection( - f'ws://{container.get_container_host_ip()}:{container.get_exposed_port(8182)}/gremlin', - 'g', - message_serializer=JanusGraphSONSerializersV3d0() - ) - g = traversal().with_remote(connection) - - if g.V().has('name', 'hercules').has_next(): - break - except Exception as e: - pass - finally: - if connection: - connection.close() + + def create_container(passed_request): + container = None + + def is_server_ready(): + """ + Method to test if JanusGraph server is up and running and filled with test data + """ + connection = None - time.sleep(2) + while True: + try: + connection = DriverRemoteConnection( + f'ws://{container.get_container_host_ip()}:{container.get_exposed_port(8182)}/gremlin', + 'g', + message_serializer=JanusGraphSONSerializersV3d0() + ) + g = traversal().with_remote(connection) - config = configparser.ConfigParser() - config.read(os.path.join(current_path, 'config.ini')) + if g.V().has('name', 'hercules').has_next(): + break + except Exception as e: + pass + finally: + if connection: + connection.close() - container = ( - DockerContainer(config['docker']['image']) - .with_name('janusgraph') - .with_exposed_ports(8182) - .with_volume_mapping(os.path.join(current_path, 'load_data.groovy'), '/docker-entrypoint-initdb.d/load_data.groovy') - .start() - ) - is_server_ready() + time.sleep(2) - def drop_container(): - container.stop() - - request.addfinalizer(drop_container) + if not hasattr(passed_request, "param"): + top_fixture = passed_request.fixturenames[0] + exit(f"{top_fixture} fixture needs to be parametrized with the list of JanusGraph versions to test with") + + tag = passed_request.param + image = f"{JANUSGRAPH_REPOSITORY}:{tag}" + + container = ( + DockerContainer(image) + .with_name(f'janusgraph_{tag}') + .with_exposed_ports(8182) + .with_volume_mapping(os.path.join(current_path, 'load_data.groovy'), '/docker-entrypoint-initdb.d/load_data.groovy') + .start() + ) + is_server_ready() + + def drop_container(): + container.stop() + + request.addfinalizer(drop_container) - return container + return container + return create_container \ No newline at end of file diff --git a/tests/integration/io/test_graphsonV3d0.py b/tests/integration/io/test_graphsonV3d0.py index 18eb1ef..0fec8e2 100644 --- a/tests/integration/io/test_graphsonV3d0.py +++ b/tests/integration/io/test_graphsonV3d0.py @@ -12,10 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -from pytest import fixture +from pytest import fixture, mark from integration.RelationIdentifier_test import _RelationIdentifierSerializer, _RelationIdentifierDeserializer from integration.Text_test import _TextTests +from ..conftest import JANUSGRAPH_VERSION_PARAMS + +# parametrize all integration tests to run against various JanusGraph versions +pytestmark = mark.parametrize( + "graph_connection_graphson", JANUSGRAPH_VERSION_PARAMS, indirect=True +) class TestGraphSONRelationIdentifierSerializer(_RelationIdentifierSerializer): @fixture(autouse=True) diff --git a/tests/unit/process/test_traversal.py b/tests/unit/process/test_traversal.py index 82c7888..51aa4ff 100644 --- a/tests/unit/process/test_traversal.py +++ b/tests/unit/process/test_traversal.py @@ -219,12 +219,21 @@ class TestText(object): 'predicate,expected', [ param(Text.text_contains('John'), 'textContains(John)', id='text_contains'), + param(Text.text_not_contains('John'), 'textNotContains(John)', id='text_not_contains'), param(Text.text_contains_prefix('John'), 'textContainsPrefix(John)', id='text_contains_prefix'), + param(Text.text_not_contains_prefix('John'), 'textNotContainsPrefix(John)', id='text_not_contains_prefix'), param(Text.text_contains_fuzzy('Juhn'), 'textContainsFuzzy(Juhn)', id='text_contains_fuzzy'), + param(Text.text_not_contains_fuzzy('Juhn'), 'textNotContainsFuzzy(Juhn)', id='text_not_contains_fuzzy'), param(Text.text_contains_regex('.*hn.*'), 'textContainsRegex(.*hn.*)', id='text_contains_regex'), + param(Text.text_not_contains_regex('.*hn.*'), 'textNotContainsRegex(.*hn.*)', id='text_not_contains_regex'), + param(Text.text_contains_phrase('John Doe'), 'textContainsPhrase(John Doe)', id='text_contains_phrase'), + param(Text.text_not_contains_phrase('John Doe'), 'textNotContainsPhrase(John Doe)', id='text_not_contains_phrase'), param(Text.text_fuzzy('Juhn'), 'textFuzzy(Juhn)', id='text_fuzzy'), + param(Text.text_not_fuzzy('Juhn'), 'textNotFuzzy(Juhn)', id='text_not_fuzzy'), param(Text.text_prefix('John'), 'textPrefix(John)', id='text_prefix'), + param(Text.text_not_prefix('John'), 'textNotPrefix(John)', id='text_not_prefix'), param(Text.text_regex('.*hn.*'), 'textRegex(.*hn.*)', id='text_regex'), + param(Text.text_not_regex('.*hn.*'), 'textNotRegex(.*hn.*)', id='text_not_regex'), ] ) def test_Text(self, predicate, expected):