├── tests ├── __init__.py ├── test_server.py ├── test_modern.py ├── base_gremlin_test.py ├── basetest.py ├── test_003_connection.py ├── test_004_io.py ├── test_examples.py ├── test_draw.py ├── test_005_graphviz.py ├── test_tutorial.py └── jupyter.ipynb ├── gremlin ├── __init__.py ├── examples.py ├── remote.py └── draw.py ├── scripts ├── install ├── blackisort ├── runDataStax ├── release ├── runOrientDB ├── installAndTest ├── runNeo4j ├── doc ├── test └── run ├── config ├── Neo4j.yaml ├── DataStax.yaml ├── server.yaml ├── TinkerGraph.yaml └── OrientDB.yaml ├── mkdocs.yml ├── .pydevproject ├── .project ├── .travis.yml ├── .github └── workflows │ ├── upload-to-pypi.yml │ └── build.yml ├── README.md ├── setServer.py ├── data └── tinkerpop-modern.xml ├── .gitignore ├── pyproject.toml └── LICENSE /tests/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gremlin/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "0.1.0" 2 | -------------------------------------------------------------------------------- /scripts/install: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2020-03-25 3 | pip install . -------------------------------------------------------------------------------- /scripts/blackisort: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2024-08-22 3 | for package in tests gremlin 4 | do 5 | isort $package/*.py 6 | black $package/*.py 7 | done 8 | -------------------------------------------------------------------------------- /scripts/runDataStax: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/datastax 2 | image=datastax/dse-server:6.7.2 3 | docker pull $image 4 | docker run --name datastax -e DS_LICENSE=accept -p 8182:8182 $image 5 | -------------------------------------------------------------------------------- /scripts/release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2024-07-31 3 | # prepare a release 4 | scripts/doc -d 5 | 6 | # Commit with a message that includes the current ISO timestamp 7 | git commit -a -m "release commit" 8 | git push 9 | -------------------------------------------------------------------------------- /config/Neo4j.yaml: -------------------------------------------------------------------------------- 1 | !!python/object:gremlin.remote.Server 2 | alias: g 3 | helpUrl: http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases 4 | host: localhost 5 | name: Neo4j 6 | password: neo4j 7 | port: 8182 8 | username: neo4j 9 | -------------------------------------------------------------------------------- /config/DataStax.yaml: -------------------------------------------------------------------------------- 1 | !!python/object:gremlin.remote.Server 2 | alias: g 3 | helpUrl: http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases 4 | host: localhost 5 | name: DataStax 6 | username: null 7 | password: null 8 | port: 8182 9 | -------------------------------------------------------------------------------- /config/server.yaml: -------------------------------------------------------------------------------- 1 | !!python/object:gremlin.remote.Server 2 | alias: g 3 | helpUrl: http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases 4 | host: localhost 5 | name: TinkerGraph 6 | password: null 7 | port: 8182 8 | username: null 9 | -------------------------------------------------------------------------------- /config/TinkerGraph.yaml: -------------------------------------------------------------------------------- 1 | !!python/object:gremlin.remote.Server 2 | alias: g 3 | helpUrl: http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases 4 | host: localhost 5 | name: TinkerGraph 6 | password: null 7 | port: 8182 8 | username: null 9 | -------------------------------------------------------------------------------- /scripts/runOrientDB: -------------------------------------------------------------------------------- 1 | # https://hub.docker.com/_/orientdb 2 | # see https://github.com/orientechnologies/orientdb-gremlin/issues/143 3 | image=orientdb:3.0.23-tp3 4 | #image=orientdb:3.0.17-tp3 5 | docker pull $image 6 | docker run -d --name odbtp3 -p 2424:2424 -p 2480:2480 -p 8182:8182 -e ORIENTDB_ROOT_PASSWORD=rootpwd $image 7 | -------------------------------------------------------------------------------- /config/OrientDB.yaml: -------------------------------------------------------------------------------- 1 | !!python/object:gremlin.remote.Server 2 | alias: g 3 | helpUrl: http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases 4 | host: localhost 5 | name: OrientDB 6 | password: rootpwd 7 | port: 8182 8 | username: root 9 | serializer: { className: org.apache.tinkerpop.gremlin. driver.ser.GraphSONMessageSerializerV1d0, config: { serializeResultToString: true }} 10 | -------------------------------------------------------------------------------- /tests/test_server.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2023-05-17 3 | 4 | @author: wf 5 | """ 6 | 7 | from tests.base_gremlin_test import BaseGremlinTest 8 | 9 | 10 | class TestServer(BaseGremlinTest): 11 | """ 12 | test server is available 13 | """ 14 | 15 | def testSocket(self): 16 | """ 17 | test socket open 18 | """ 19 | is_open = self.remote_traversal.server.check_socket() 20 | self.assertTrue(is_open) 21 | pass 22 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | site_name: gremlin-python-tutorial API Documentation 2 | theme: 3 | name: material 4 | plugins: 5 | - search 6 | - mkdocstrings: 7 | handlers: 8 | python: 9 | setup_commands: 10 | - import sys 11 | - import os 12 | - sys.path.insert(0, os.path.abspath(".")) 13 | selection: 14 | docstring_style: google 15 | rendering: 16 | show_source: true 17 | nav: 18 | - API: index.md 19 | -------------------------------------------------------------------------------- /.pydevproject: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Default 5 | 6 | python interpreter 7 | 8 | 9 | /${PROJECT_DIR_NAME} 10 | 11 | 12 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | gremlin-python-tutorial 4 | 5 | 6 | 7 | 8 | 9 | org.python.pydev.PyDevBuilder 10 | 11 | 12 | 13 | 14 | org.eclipse.wst.validation.validationbuilder 15 | 16 | 17 | 18 | 19 | 20 | org.eclipse.wst.jsdt.core.jsNature 21 | org.python.pydev.pythonNature 22 | 23 | 24 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # see https://docs.travis-ci.com/user/languages/python/ 2 | language: python 3 | # python versions to be tested 4 | python: 5 | # - "2.7" # see https://github.com/WolfgangFahl/gremlin-python-tutorial/issues/7 6 | - "3.7" 7 | before_install: 8 | # We need GraphViz to draw 9 | - "sudo apt-get install graphviz" 10 | # make sure the installation and server is run before starting the tests 11 | before_script: 12 | # install gremlin-server and python-gremlin 13 | - ./run -i 14 | # start server 15 | - ./run -s& 16 | # give server some time to start 17 | - "sleep 10" 18 | # command to install dependencies 19 | install: 20 | - pip install -r requirements.txt 21 | # command to run tests 22 | script: 23 | - pytest 24 | -------------------------------------------------------------------------------- /.github/workflows/upload-to-pypi.yml: -------------------------------------------------------------------------------- 1 | name: Upload Python Package 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | deploy: 9 | runs-on: ubuntu-latest 10 | permissions: 11 | # IMPORTANT: this permission is mandatory for trusted publishing 12 | id-token: write 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: '3.x' 19 | - name: Install dependencies 20 | run: | 21 | python -m pip install --upgrade pip 22 | pip install hatch 23 | - name: Build and publish 24 | run: | 25 | hatch build 26 | - name: Publish distribution to PyPI 27 | uses: pypa/gh-action-pypi-publish@release/v1 28 | -------------------------------------------------------------------------------- /tests/test_modern.py: -------------------------------------------------------------------------------- 1 | # see https://github.com/WolfgangFahl/gremlin-python-tutorial/blob/master/test_001.py 2 | from tests.base_gremlin_test import BaseGremlinTest 3 | 4 | 5 | class TestModern(BaseGremlinTest): 6 | """ 7 | test Remote Traversal 8 | """ 9 | 10 | def test_load_modern(self): 11 | """ 12 | test loading the tinkerpop-modern graph 13 | """ 14 | g = self.g 15 | self.examples.load_by_name(g, "tinkerpop-modern") 16 | vCount = g.V().count().next() 17 | if self.debug: 18 | print("g.V().count=%d" % (vCount)) 19 | self.assertEqual(6, vCount) 20 | eCount = g.E().count().next() 21 | if self.debug: 22 | print("g.E().count=%d" % (eCount)) 23 | self.assertEquals(6, eCount) 24 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | #os: [ubuntu-latest, macos-latest, windows-latest] 14 | #python-version: [ '3.9', '3.10', '3.11', '3.12' ] 15 | os: [ubuntu-latest] 16 | python-version: [ '3.10' ] 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | - name: Setup Graphviz 21 | uses: ts-graphviz/setup-graphviz@v1 22 | - name: Set up Python ${{ matrix.python-version }} 23 | uses: actions/setup-python@v5 24 | with: 25 | python-version: ${{ matrix.python-version }} 26 | - name: Install dependencies and test 27 | run: | 28 | scripts/installAndTest 29 | -------------------------------------------------------------------------------- /tests/base_gremlin_test.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2023-03-23 3 | 4 | @author: wf 5 | """ 6 | 7 | from gremlin.examples import Examples, Volume 8 | from gremlin.remote import RemoteTraversal, Server 9 | from tests.basetest import Basetest 10 | 11 | 12 | class BaseGremlinTest(Basetest): 13 | """ 14 | Basetest for Gremlin Python 15 | """ 16 | 17 | def setUp(self, debug=False, profile=True): 18 | """ 19 | prepare the test environment 20 | """ 21 | Basetest.setUp(self, debug, profile) 22 | self.server = Server() 23 | self.remote_traversal = RemoteTraversal(self.server) 24 | self.g = self.remote_traversal.g() 25 | self.volume = Volume.docker() 26 | self.examples = Examples(volume=self.volume, debug=self.debug) 27 | 28 | def tearDown(self): 29 | """ 30 | tear down 31 | """ 32 | Basetest.tearDown(self) 33 | self.remote_traversal.close() 34 | -------------------------------------------------------------------------------- /scripts/installAndTest: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2022-01-21 3 | 4 | # 5 | # install the python librarx 6 | # 7 | install() { 8 | pip install . 9 | } 10 | 11 | # 12 | # run the server 13 | # 14 | run_server_docker() { 15 | docker ps | grep gremlin-server 16 | if [ $? -eq 0 ] 17 | then 18 | docker stop gremlin-server 19 | docker rm gremlin-server 20 | fi 21 | if [ -f nohup.out ] 22 | then 23 | rm nohup.out 24 | fi 25 | nohup scripts/run -sd >nohup.out 2>&1 & 26 | } 27 | 28 | # run the server locally 29 | run_server() { 30 | rm nohup.out 31 | nohup scripts/run -s& 32 | } 33 | 34 | # wait for server for the given number of seconds 35 | # 36 | # Args: 37 | # 1: l_wait_time - wait time in seconds 38 | wait_for_server() { 39 | local l_wait_time="$1" 40 | echo "waiting up to $l_wait_time secs for server" 41 | while [ $l_wait_time -gt 1 ] 42 | do 43 | sleep 1 44 | grep "Channel started at port 8182" nohup.out 45 | if [ $? -eq 0 ] 46 | then 47 | cat nohup.out 48 | return 49 | fi 50 | l_wait_time=$((l_wait_time-1)) 51 | echo "$l_wait_time secs left ..." 52 | done 53 | echo "Server start failed" 54 | cat nohup.out 55 | exit 1 56 | } 57 | install 58 | run_server_docker 59 | wait_for_server 20 60 | 61 | scripts/test 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![pypi](https://img.shields.io/pypi/pyversions/gremlin-python-tutorial)](https://pypi.org/project/gremlin-python-tutorial/) 2 | [![PyPI Status](https://img.shields.io/pypi/v/gremlin-python-tutorial.svg)](https://pypi.python.org/pypi/gremlin-python-tutorial/) 3 | [![Github Actions Build](https://github.com/WolfgangFahl/gremlin-python-tutorial/actions/workflows/build.yml/badge.svg)](https://github.com/WolfgangFahl/gremlin-python-tutorial/actions/workflows/build.yml) 4 | [![GitHub issues](https://img.shields.io/github/issues/WolfgangFahl/gremlin-python-tutorial.svg)](https://github.com/WolfgangFahl/gremlin-python-tutorial/issues) 5 | [![GitHub closed issues](https://img.shields.io/github/issues-closed/WolfgangFahl/gremlin-python-tutorial.svg)](https://github.com/WolfgangFahl/gremlin-python-tutorial/issues/?q=is%3Aissue+is%3Aclosed) 6 | [![API Docs](https://img.shields.io/badge/API-Documentation-blue)](https://WolfgangFahl.github.io/gremlin-python-tutorial/) 7 | [![License](https://img.shields.io/github/license/WolfgangFahl/gremlin-python-tutorial.svg)](https://www.apache.org/licenses/LICENSE-2.0) 8 | [BITPlan](http://www.bitplan.com) 9 | # gremlin-python-tutorial 10 | Gremlin-Python tutorial 11 | 12 | ## Documentation 13 | * [Wiki](http://wiki.bitplan.com/index.php/Gremlin_python) 14 | -------------------------------------------------------------------------------- /setServer.py: -------------------------------------------------------------------------------- 1 | # set the server configuration 2 | import argparse 3 | from gremlin import gremote 4 | 5 | # https://docs.python.org/2/library/argparse.html 6 | # prepare command line argument accepted 7 | parser = argparse.ArgumentParser(description='set the server configuration for gremlin_python') 8 | parser.add_argument('--debug',action="store_true",help='show debug info') 9 | parser.add_argument('--rewrite',action="store_true",help='write a new server configuration') 10 | parser.add_argument('--host', default="localhost", help='the host name of the server') 11 | parser.add_argument('--port',type=int, default=8182,help='the port to be used') 12 | parser.add_argument('--alias', default="g", help='the default alias to use') 13 | parser.add_argument('--name', default="TinkerGraph", help='the name of the server') 14 | parser.add_argument('--username', default=None, help='the username to use for authentication') 15 | parser.add_argument('--password', default=None, help='the password to use for authentication') 16 | parser.add_argument('--helpUrl', default="http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases", help='the url for help on this server configuration') 17 | 18 | # parse the command line arguments 19 | args = parser.parse_args() 20 | 21 | # uncomment to debug 22 | gremote.Server.debug=True 23 | # try reading the server description from the yaml file with the given name 24 | server=gremote.Server.read(args.name) 25 | if server is None or args.rewrite: 26 | server=gremote.Server(host=args.host,port=args.port,alias=args.alias,name=args.name,username=args.username,password=args.password,debug=args.debug) 27 | server.write() 28 | -------------------------------------------------------------------------------- /tests/basetest.py: -------------------------------------------------------------------------------- 1 | import getpass 2 | import time 3 | from unittest import TestCase 4 | 5 | 6 | class Basetest(TestCase): 7 | """ 8 | base test case 9 | """ 10 | 11 | def setUp(self, debug=False, profile=True): 12 | """ 13 | setUp test environment 14 | """ 15 | TestCase.setUp(self) 16 | self.debug = debug 17 | self.profile = profile 18 | msg = f"test {self._testMethodName}, debug={self.debug}" 19 | self.profiler = Profiler(msg, profile=self.profile) 20 | 21 | def tearDown(self): 22 | TestCase.tearDown(self) 23 | self.profiler.time() 24 | 25 | def inPublicCI(self): 26 | """ 27 | are we running in a public Continuous Integration Environment? 28 | """ 29 | return getpass.getuser() in ["travis", "runner"] 30 | 31 | 32 | class Profiler: 33 | """ 34 | simple profiler 35 | """ 36 | 37 | def __init__(self, msg, profile=True): 38 | """ 39 | construct me with the given msg and profile active flag 40 | 41 | Args: 42 | msg(str): the message to show if profiling is active 43 | profile(bool): True if messages should be shown 44 | """ 45 | self.msg = msg 46 | self.profile = profile 47 | self.starttime = time.time() 48 | if profile: 49 | print(f"Starting {msg} ...") 50 | 51 | def time(self, extraMsg=""): 52 | """ 53 | time the action and print if profile is active 54 | """ 55 | elapsed = time.time() - self.starttime 56 | if self.profile: 57 | print(f"{self.msg}{extraMsg} took {elapsed:5.1f} s") 58 | return elapsed 59 | -------------------------------------------------------------------------------- /tests/test_003_connection.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/jython/tests/driver/test_client.py 3 | from os.path import abspath, dirname 4 | 5 | from gremlin_python.driver.request import RequestMessage 6 | 7 | from gremlin.remote import RemoteTraversal, Server 8 | from tests.basetest import Basetest 9 | 10 | 11 | class TestConnection(Basetest): 12 | """ 13 | test connection handling 14 | """ 15 | 16 | # test a connection 17 | def test_connection(self): 18 | # see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/jython/gremlin_python/driver/driver_remote_connection.py 19 | server = Server() 20 | remote_traversal = RemoteTraversal(server) 21 | g = remote_traversal.g() 22 | t = g.V() 23 | remoteConnection = remote_traversal.remoteConnection 24 | # see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/jython/gremlin_python/driver/client.py 25 | client = remoteConnection._client 26 | 27 | connection = client._get_connection() 28 | message = RequestMessage( 29 | "traversal", 30 | "bytecode", 31 | {"gremlin": t.bytecode, "aliases": {"g": client._traversal_source}}, 32 | ) 33 | results_set = connection.write(message).result() 34 | future = results_set.all() 35 | results = future.result() 36 | print("%d results" % (len(results))) 37 | # assert len(results) == 6 38 | assert isinstance(results, list) 39 | assert results_set.done.done() 40 | # assert 'host' in results_set.status_attributes 41 | print(results_set.status_attributes) 42 | connection.close() 43 | -------------------------------------------------------------------------------- /tests/test_004_io.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from tests.base_gremlin_test import BaseGremlinTest 4 | 5 | 6 | class TestIo(BaseGremlinTest): 7 | """ 8 | test Io handling 9 | """ 10 | 11 | # test loading a graph 12 | def test_loadGraph(self): 13 | g = self.g 14 | airroutes = "air-routes-small" 15 | self.examples.load_by_name(g, f"{airroutes}") 16 | graphmlFile = f"{self.volume.remote_path}/{airroutes}.xml" 17 | # make the local file accessible to the server 18 | airRoutesPath = os.path.abspath(graphmlFile) 19 | # drop the existing content of the graph 20 | g.V().drop().iterate() 21 | # read the content from the air routes example 22 | g.io(airRoutesPath).read().iterate() 23 | vCount = g.V().count().next() 24 | if self.debug: 25 | print(f"{graphmlFile} has {vCount} vertices") 26 | assert vCount == 47 27 | 28 | # test saving a graph 29 | def test_saveGraph(self): 30 | g = self.g 31 | graphMl = "/tmp/a_fish_named_wanda.xml" 32 | # drop the existing content of the graph 33 | g.V().drop().iterate() 34 | g.addV("Fish").property("name", "Wanda").iterate() 35 | g.io(graphMl).write().iterate() 36 | if self.debug: 37 | print(f"wrote graph to {graphMl}") 38 | g.V().drop().iterate() 39 | g.io(graphMl).read().iterate() 40 | vCount = g.V().count().next() 41 | debug = self.debug 42 | debug = True 43 | if debug: 44 | print(f"{graphMl} has {vCount} vertices") 45 | assert vCount == 1 46 | # check that the graphml file exists 47 | # unfortunately this doesn't work as of 2023-06-11 48 | # in the github CI 49 | # assert os.path.isfile(self.volume.local(graphMl)) 50 | -------------------------------------------------------------------------------- /data/tinkerpop-modern.xml: -------------------------------------------------------------------------------- 1 | personmarko29personvadas27softwarelopjavapersonjosh32softwareripplejavapersonpeter35knows0.5knows1.0created0.4created1.0created0.4created0.2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | 28 | # PyInstaller 29 | # Usually these files are written by a python script from a template 30 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 31 | *.manifest 32 | *.spec 33 | 34 | # Installer logs 35 | pip-log.txt 36 | pip-delete-this-directory.txt 37 | 38 | # Unit test / coverage reports 39 | htmlcov/ 40 | .tox/ 41 | .coverage 42 | .coverage.* 43 | .cache 44 | nosetests.xml 45 | coverage.xml 46 | *.cover 47 | .hypothesis/ 48 | .pytest_cache/ 49 | 50 | # Translations 51 | *.mo 52 | *.pot 53 | 54 | # Django stuff: 55 | *.log 56 | local_settings.py 57 | db.sqlite3 58 | 59 | # Flask stuff: 60 | instance/ 61 | .webassets-cache 62 | 63 | # Scrapy stuff: 64 | .scrapy 65 | 66 | # Sphinx documentation 67 | docs/_build/ 68 | 69 | # PyBuilder 70 | target/ 71 | 72 | # Jupyter Notebook 73 | .ipynb_checkpoints 74 | 75 | # pyenv 76 | .python-version 77 | 78 | # celery beat schedule file 79 | celerybeat-schedule 80 | 81 | # SageMath parsed files 82 | *.sage.py 83 | 84 | # Environments 85 | .env 86 | .venv 87 | env/ 88 | venv/ 89 | ENV/ 90 | env.bak/ 91 | venv.bak/ 92 | 93 | # Spyder project settings 94 | .spyderproject 95 | .spyproject 96 | 97 | # Rope project settings 98 | .ropeproject 99 | 100 | # mkdocs documentation 101 | /site 102 | 103 | # mypy 104 | .mypy_cache/ 105 | 106 | 107 | # zip files 108 | .zip 109 | 110 | # apache tinkerpop binary directories 111 | apache-tinkerpop-gremlin-* 112 | 113 | # nohup.out files 114 | nohup.out 115 | 116 | # eclipse settings directory 117 | .settings 118 | 119 | # VSCode files 120 | .vscode/ 121 | 122 | # mkdocs 123 | docs 124 | -------------------------------------------------------------------------------- /tests/test_examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2023-01-15 3 | 4 | @author: wf 5 | """ 6 | 7 | from tests.base_gremlin_test import BaseGremlinTest 8 | 9 | 10 | class TestExamples(BaseGremlinTest): 11 | """ 12 | test the Examples 13 | 14 | """ 15 | 16 | def setUp(self, debug=False, profile=True): 17 | BaseGremlinTest.setUp(self, debug=debug, profile=profile) 18 | # if not self.inPublicCI(): 19 | # self.examples.remote_examples_path=self.examples.local_examples_path 20 | 21 | def test_modern(self): 22 | """ 23 | test loading the modern graph 24 | """ 25 | g = self.g 26 | self.examples.load_by_name(g, "tinkerpop-modern") 27 | v_count = g.V().count().next() 28 | g.V().toList() 29 | if self.debug: 30 | print(f"graph imported has {v_count} vertices") 31 | assert v_count == 6 32 | 33 | def test_grateful_dead(self): 34 | """ 35 | test loading grateful dead example 36 | """ 37 | g = self.g 38 | self.examples.load_by_name(g, "grateful-dead") 39 | v_count = g.V().count().next() 40 | g.V().toList() 41 | if self.debug: 42 | print(f"graph imported has {v_count} vertices") 43 | assert v_count == 808 44 | 45 | def test_air_routes_small(self): 46 | """ 47 | test air routes example 48 | """ 49 | g = self.g 50 | self.examples.load_by_name(g, "air-routes-small") 51 | v_count = g.V().count().next() 52 | g.V().toList() 53 | if self.debug: 54 | print(f"graph imported has {v_count} vertices") 55 | assert v_count == 47 56 | 57 | def test_air_route_latest(self): 58 | """ 59 | test air route latest 60 | """ 61 | g = self.g 62 | self.examples.load_by_name(g, "air-routes-latest") 63 | v_count = g.V().count().next() 64 | g.V().toList() 65 | if self.debug: 66 | print(f"graph imported has {v_count} vertices") 67 | assert v_count == 3749 68 | -------------------------------------------------------------------------------- /tests/test_draw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2023-05-15 3 | 4 | @author: wf 5 | """ 6 | 7 | import unittest 8 | 9 | from gremlin.draw import GremlinDraw 10 | from gremlin.examples import Examples 11 | from tests.base_gremlin_test import BaseGremlinTest 12 | 13 | 14 | class TestDraw(BaseGremlinTest): 15 | """ 16 | test graphviz draw access 17 | """ 18 | 19 | def check_draw(self, gviz): 20 | """ 21 | check the drawing result 22 | """ 23 | debug = self.debug 24 | debug = True 25 | if debug: 26 | print(gviz.source) 27 | 28 | def testDraw(self): 29 | """ 30 | test creating a graphviz graph from a gremlin graph 31 | """ 32 | g = self.g 33 | self.examples.load_by_name(g, "tinkerpop-modern") 34 | gviz = GremlinDraw.show(g) 35 | self.check_draw(gviz) 36 | self.assertEqual(12, len(gviz.body)) 37 | 38 | def testDrawTraversal(self): 39 | """ 40 | test drawing a traversal 41 | """ 42 | g = self.g 43 | self.examples.load_by_name(g, "tinkerpop-modern") 44 | traversal = g.E().hasLabel("created").toList() 45 | gviz = GremlinDraw.show_graph_traversal(g, traversal, "software") 46 | self.check_draw(gviz) 47 | 48 | def testDrawLimited(self): 49 | """ 50 | test the limited drawing 51 | """ 52 | g = self.g 53 | self.examples.load_by_name(g, "air-routes-latest") 54 | # see https://www.kelvinlawrence.net/book/PracticalGremlin.html#continentdist 55 | traversal = ( 56 | g.V() 57 | .has("continent", "code", "EU") 58 | .outE() 59 | .as_("contains") 60 | .inV() 61 | .outE() 62 | .as_("route") 63 | .inV() 64 | .has("country", "US") 65 | .select("route") 66 | .toList() 67 | ) 68 | 69 | self.assertTrue(len(traversal) >= 435) 70 | gd = GremlinDraw(g, title="EU - US Flights") 71 | gd.config.v_limit = 5 72 | gd.config.vertex_properties = ["country", "code", "city"] # [""] 73 | gd.draw(traversal) 74 | self.check_draw(gd.gviz) 75 | 76 | 77 | if __name__ == "__main__": 78 | # import sys;sys.argv = ['', 'Test.testName'] 79 | unittest.main() 80 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | # see https://flit.pypa.io/en/latest/pyproject_toml.html 2 | [build-system] 3 | #requires = ["flit_core >=3.2,<4"] 4 | #build-backend = "flit_core.buildapi" 5 | requires = ["hatchling"] 6 | build-backend = "hatchling.build" 7 | 8 | [project] 9 | name = "gremlin-python-tutorial" 10 | authors = [ 11 | {name = "Wolfgang Fahl", email = "wf@bitplan.com"}, 12 | {name = "Julian Vollmer", email = "julian.vollmer@rwth-aachen.de"} 13 | ] 14 | maintainers = [ 15 | { name = "Wolfgang Fahl", email = "wf@bitplan.com" }, 16 | {name = "Julian Vollmer", email = "julian.vollmer@rwth-aachen.de"} 17 | ] 18 | readme = "README.md" 19 | # flit_core.config.ConfigError: license field should be , not 20 | license = { file="LICENSE" } 21 | dependencies = [ 22 | # https://pypi.org/project/gremlinpython/ 23 | 'gremlinpython>=3.7.3', 24 | # https://pypi.org/project/graphviz/ 25 | 'graphviz>=0.20.3', 26 | # https://pypi.org/project/PyYAML/ 27 | 'PyYAML>=6.0.2', 28 | # https://pypi.org/project/aenum/ 29 | 'aenum>=3.1.15', 30 | ] 31 | 32 | requires-python = ">=3.9" 33 | classifiers=[ 34 | "Development Status :: 4 - Beta", 35 | "Environment :: Web Environment", 36 | "Programming Language :: Python :: 3 :: Only", 37 | "Programming Language :: Python :: 3.9", 38 | "Programming Language :: Python :: 3.10", 39 | "Programming Language :: Python :: 3.11", 40 | "Programming Language :: Python :: 3.12", 41 | "Operating System :: OS Independent", 42 | "Topic :: Software Development", 43 | "Intended Audience :: Developers", 44 | "Intended Audience :: Science/Research", 45 | "License :: OSI Approved :: Apache Software License" 46 | ] 47 | dynamic = ["version", "description"] 48 | 49 | [tool.hatch.version] 50 | path = "gremlin/__init__.py" 51 | 52 | [tool.hatch.build.targets.wheel] 53 | only-include = ["gremlin", "config", "data"] 54 | 55 | [tool.hatch.build.targets.wheel.sources] 56 | "gremlin" = "gremlin" 57 | "config" = "gremlin/config" 58 | "data" = "gremlin/data" 59 | 60 | [project.urls] 61 | Home = "https://github.com/WolfgangFahl/gremlin-python-tutorial" 62 | Documentation = "https://wiki.bitplan.com/index.php/Gremlin_python" 63 | Source = "https://github.com/WolfgangFahl/gremlin-python-tutorial" 64 | 65 | [project.optional-dependencies] 66 | test = [ 67 | "green", 68 | ] 69 | -------------------------------------------------------------------------------- /tests/test_005_graphviz.py: -------------------------------------------------------------------------------- 1 | # see https://github.com/WolfgangFahl/gremlin-python-tutorial/blob/master/test_005_graphviz.py 2 | import os.path 3 | import platform 4 | 5 | from graphviz import Digraph 6 | from gremlin_python.process.traversal import T 7 | 8 | from tests.base_gremlin_test import BaseGremlinTest 9 | 10 | 11 | class TestGraphvizGraph(BaseGremlinTest): 12 | """ 13 | test creating a graphviz graph from the tinkerpop graph 14 | """ 15 | 16 | def test_createGraphvizGraph(self): 17 | # make sure we re-load the tinkerpop modern example 18 | g = self.g 19 | self.examples.load_by_name(g, f"tinkerpop-modern") 20 | # start a graphviz 21 | dot = Digraph(comment="Modern") 22 | # get vertice properties including id and label as dicts 23 | for vDict in g.V().valueMap(True).toList(): 24 | # uncomment to debug 25 | # print vDict 26 | # get id and label 27 | vId = vDict[T.id] 28 | vLabel = vDict[T.label] 29 | # greate a graphviz node label 30 | # name property is alway there 31 | gvLabel = r"%s\n%s\nname=%s" % (vId, vLabel, vDict["name"][0]) 32 | # if there is an age property add it to the label 33 | if "age" in vDict: 34 | gvLabel = gvLabel + r"\nage=%s" % (vDict["age"][0]) 35 | # create a graphviz node 36 | dot.node("node%d" % (vId), gvLabel) 37 | # loop over all edges 38 | for e in g.E(): 39 | # get the detail information with a second call per edge (what a pitty to be so inefficient ...) 40 | eDict = g.E(e.id).valueMap(True).next() 41 | # uncomment if you'd like to debug 42 | # print (e,eDict) 43 | # create a graphviz label 44 | geLabel = r"%s\n%s\nweight=%s" % (e.id, e.label, eDict["weight"]) 45 | # add a graphviz edge 46 | dot.edge("node%d" % (e.outV.id), "node%d" % (e.inV.id), label=geLabel) 47 | # modify the styling see http://www.graphviz.org/doc/info/attrs.html 48 | dot.edge_attr.update(arrowsize="2", penwidth="2") 49 | dot.node_attr.update(style="filled", fillcolor="#A8D0E4") 50 | # print the source code 51 | if self.debug: 52 | print(dot.source) 53 | if platform.system() == "Darwin": # Darwin represents macOS 54 | os.environ["PATH"] += os.pathsep + "/opt/local/bin" 55 | 56 | # render without viewing - default is creating a pdf file 57 | dot.render("/tmp/modern.gv", view=False) 58 | # check that the pdf file exists 59 | assert os.path.isfile("/tmp/modern.gv.pdf") 60 | -------------------------------------------------------------------------------- /scripts/runNeo4j: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # https://hub.docker.com/_/neo4j 3 | # WF 2017-07-05 4 | # added to gremlin-python tutorial 2019-09-21 5 | image=neo4j:3.5.9 6 | containername=neo4j 7 | 8 | #ansi colors 9 | #http://www.csc.uvic.ca/~sae/seng265/fall04/tips/s265s047-tips/bash-using-colors.html 10 | blue='\033[0;34m' 11 | red='\033[0;31m' 12 | green='\033[0;32m' # '\e[1;32m' is too bright for white bg. 13 | endColor='\033[0m' 14 | 15 | # 16 | # a colored message 17 | # params: 18 | # 1: l_color - the color of the message 19 | # 2: l_msg - the message to display 20 | # 21 | color_msg() { 22 | local l_color="$1" 23 | local l_msg="$2" 24 | echo -e "${l_color}$l_msg${endColor}" 25 | } 26 | 27 | # 28 | # error 29 | # 30 | # show an error message and exit 31 | # 32 | # params: 33 | # 1: l_msg - the message to display 34 | error() { 35 | local l_msg="$1" 36 | # use ansi red for error 37 | color_msg $red "Error: $l_msg" 1>&2 38 | exit 1 39 | } 40 | 41 | # 42 | # show usage 43 | # 44 | usage() { 45 | local p_name=`basename $0` 46 | echo "$p_name" 47 | echo " -h |--help : show this usage" 48 | echo " -rc|--recreate : recreate the container" 49 | echo " -it : run shell in container" 50 | exit 1 51 | } 52 | 53 | # remember the time we started this 54 | start_date=$(date -u +"%s") 55 | 56 | while test $# -gt 0 57 | do 58 | case $1 in 59 | # help 60 | -h|--help) 61 | usage;; 62 | 63 | -rc|--recreate) 64 | recreate="true" 65 | ;; 66 | 67 | -it) 68 | it=true 69 | ;; 70 | esac 71 | shift 72 | done 73 | 74 | # prepare data directory (if not there yet) 75 | data=/tmp/neo4j 76 | if [ ! -d $data ] 77 | then 78 | mkdir -p $data 79 | fi 80 | 81 | # 82 | # prepare optional recreate 83 | # 84 | if [ "$recreate" = "true" ] 85 | then 86 | color_msg $blue "preparing recreate of $containername" 87 | color_msg $blue "stopping $containername" 88 | docker stop $containername 89 | color_msg $blue "remove $containername" 90 | docker rm $containername 91 | fi 92 | 93 | docker images $image | grep neo4j 94 | if [ $? -ne 0 ] 95 | then 96 | docker pull $image 97 | fi 98 | 99 | docker ps | grep $containername 100 | if [ $? -ne 0 ] 101 | then 102 | nohup docker run \ 103 | --publish=7474:7474 --publish=7687:7687 \ 104 | --volume=$data:/data \ 105 | --name $containername \ 106 | $image& 107 | sleep 2 108 | fi 109 | #find out hostname 110 | hostname=$(hostname) 111 | 112 | # open neo4j in browser 113 | open http://$hostname:7474/ 114 | 115 | # open shell 116 | if [ "$it" == "true" ] 117 | then 118 | docker exec -it $containername /bin/bash 119 | fi 120 | -------------------------------------------------------------------------------- /scripts/doc: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # create docs for a configurable project 3 | # WF 2024-07-30 - updated 4 | 5 | # Extract project name from pyproject.toml 6 | PROJECT_NAME=$(grep "\[project\]" pyproject.toml -A1 | grep name | cut -d '=' -f2 | tr -d ' "') 7 | PACKAGE_NAME=$(grep "\[tool.hatch.build.targets.wheel.sources\]" pyproject.toml -A1 | tail -1 | cut -d '=' -f2 | tr -d ' "') 8 | 9 | 10 | # Function to print usage information 11 | print_usage() { 12 | echo "Usage: $0 [OPTIONS]" 13 | echo "Options:" 14 | echo " -pr, --project NAME Set the project name (default: $PROJECT_NAME)" 15 | echo " -pa, --package NAME Set the package name (default: $PACKAGE_NAME)" 16 | echo " -d, --deploy Deploy the documentation after building" 17 | echo " -h, --help Display this help message" 18 | } 19 | 20 | # Parse command line arguments 21 | DEPLOY=false 22 | while [[ "$#" -gt 0 ]]; do 23 | case $1 in 24 | -pr|--project) PROJECT_NAME="$2"; shift ;; 25 | -pa|--package) PACKAGE_NAME="$2"; shift ;; 26 | -d|--deploy) DEPLOY=true ;; 27 | -h|--help) print_usage; exit 0 ;; 28 | *) echo "Unknown parameter: $1"; print_usage; exit 1 ;; 29 | esac 30 | shift 31 | done 32 | 33 | # Ensure we're in the correct directory 34 | if [[ ! -d "$PACKAGE_NAME" ]]; then 35 | echo "Error: $PACKAGE_NAME package directory not found. Are you in the correct directory?" 36 | exit 1 37 | fi 38 | 39 | # Check if mkdocs is installed 40 | if ! command -v mkdocs &> /dev/null; then 41 | pip install mkdocs mkdocs-material mkdocstrings[python] 42 | fi 43 | 44 | # Create or update mkdocs.yml 45 | cat << EOF > mkdocs.yml 46 | site_name: $PROJECT_NAME API Documentation 47 | theme: 48 | name: material 49 | plugins: 50 | - search 51 | - mkdocstrings: 52 | handlers: 53 | python: 54 | setup_commands: 55 | - import sys 56 | - import os 57 | - sys.path.insert(0, os.path.abspath(".")) 58 | selection: 59 | docstring_style: google 60 | rendering: 61 | show_source: true 62 | nav: 63 | - API: index.md 64 | EOF 65 | 66 | # Create or update index.md 67 | index_md=docs/index.md 68 | mkdir -p docs 69 | cat << EOF > $index_md 70 | # $PROJECT_NAME API Documentation 71 | 72 | ::: $PACKAGE_NAME 73 | options: 74 | show_submodules: true 75 | EOF 76 | 77 | # Ignore DeprecationWarnings during build 78 | export PYTHONWARNINGS="ignore::DeprecationWarning" 79 | 80 | # Build the documentation 81 | mkdocs build --config-file ./mkdocs.yml 82 | 83 | # Deploy if requested 84 | if [ "$DEPLOY" = true ]; then 85 | mkdocs gh-deploy --force --config-file ./mkdocs.yml 86 | fi 87 | 88 | echo "Documentation process completed for $PROJECT_NAME." 89 | -------------------------------------------------------------------------------- /scripts/test: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2022-08-20 3 | # WF 2024-08-03 4 | 5 | #ansi colors 6 | #http://www.csc.uvic.ca/~sae/seng265/fall04/tips/s265s047-tips/bash-using-colors.html 7 | blue='\033[0;34m' 8 | red='\033[0;31m' 9 | green='\033[0;32m' # '\e[1;32m' is too bright for white bg. 10 | endColor='\033[0m' 11 | 12 | # 13 | # a colored message 14 | # params: 15 | # 1: l_color - the color of the message 16 | # 2: l_msg - the message to display 17 | # 18 | color_msg() { 19 | local l_color="$1" 20 | local l_msg="$2" 21 | echo -e "${l_color}$l_msg${endColor}" 22 | } 23 | 24 | # 25 | # error 26 | # 27 | # show the given error message on stderr and exit 28 | # 29 | # params: 30 | # 1: l_msg - the error message to display 31 | # 32 | error() { 33 | local l_msg="$1" 34 | # use ansi red for error 35 | color_msg $red "Error:" 1>&2 36 | color_msg $red "\t$l_msg" 1>&2 37 | exit 1 38 | } 39 | 40 | # 41 | # show a negative message 42 | # 43 | negative() { 44 | local l_msg="$1" 45 | color_msg $red "❌:$l_msg" 46 | } 47 | 48 | # 49 | # show a positive message 50 | # 51 | positive() { 52 | local l_msg="$1" 53 | color_msg $green "✅:$l_msg" 54 | } 55 | 56 | # show usage 57 | # 58 | usage() { 59 | echo "$0 [-g|--green|-m|--module|-t|--tox|-h|--help]" 60 | echo "-t |--tox: run tests with tox" 61 | echo "-g |--green: run tests with green" 62 | echo "-m |--module: run modulewise test" 63 | echo "-h |--help: show this usage" 64 | echo "default is running tests with unittest discover" 65 | exit 1 66 | } 67 | 68 | # 69 | # check and optional install the given package 70 | # 71 | check_package() { 72 | local l_package="$1" 73 | pip show $l_package > /dev/null 74 | if [ $? -ne 0 ] 75 | then 76 | negative "$l_package" 77 | color_msg $blue "installing $l_package" 78 | pip install $l_package 79 | else 80 | positive "$l_package" 81 | fi 82 | } 83 | 84 | # 85 | # test module by module 86 | # 87 | modulewise_test() { 88 | foundErrors=0 89 | foundTests=0 90 | for testmodule in tests/test*.py 91 | do 92 | echo "testing $testmodule ..." 93 | # see https://github.com/CleanCut/green/issues/263 94 | #green $testmodule -s1 95 | python -m unittest $testmodule 96 | exit_code=$? 97 | foundErrors=$((foundErrors+exit_code)) 98 | foundTests=$((foundTests+1)) 99 | done 100 | echo "$foundErrors/$foundTests module unit tests failed" 1>&2 101 | if [[ $foundErrors -gt 0 ]] 102 | then 103 | exit 1 104 | fi 105 | } 106 | 107 | export PYTHON_PATH="." 108 | while [ "$1" != "" ] 109 | do 110 | option="$1" 111 | case $option in 112 | -h|--help) 113 | usage 114 | exit 0 115 | ;; 116 | -g|--green) 117 | check_package green 118 | green tests -s 1 119 | exit 0 120 | ;; 121 | -m|--module) 122 | modulewise_test 123 | exit 0 124 | ;; 125 | -t|--tox) 126 | check_package tox 127 | tox -e py 128 | exit 0 129 | ;; 130 | esac 131 | done 132 | python3 -m unittest discover 133 | -------------------------------------------------------------------------------- /tests/test_tutorial.py: -------------------------------------------------------------------------------- 1 | # see 2 | # http://wiki.bitplan.com/index.php/Gremlin_python#Getting_Started 3 | from gremlin_python.process.graph_traversal import GraphTraversal 4 | 5 | from tests.base_gremlin_test import BaseGremlinTest 6 | 7 | 8 | class TestTutorial(BaseGremlinTest): 9 | """ 10 | test connection handling 11 | """ 12 | 13 | def setUp(self, debug=False, profile=True): 14 | """ 15 | setUp the test environment 16 | """ 17 | BaseGremlinTest.setUp(self, debug=debug, profile=profile) 18 | # in TinkerGraph this is the first id 19 | # get id of Marko's vertex which is usually 1 but might be different e.g. 20 | # when Neo4j is used 21 | self.examples.load_by_name(self.g, "tinkerpop-modern") 22 | l = self.g.V().toList() 23 | self.id1 = l[0].id 24 | 25 | def log(self, thing): 26 | """ 27 | convert thing to string and print out for debugging 28 | """ 29 | text = str(thing) 30 | if self.debug: 31 | print(text) 32 | return text 33 | 34 | def test_tutorial1(self): 35 | """ 36 | g.V() //(1) 37 | ==>v[1] 38 | ==>v[2] 39 | ==>v[3] 40 | ==>v[4] 41 | ==>v[5] 42 | ==>v[6] 43 | """ 44 | # get the vertices 45 | gV = self.g.V() 46 | # we have a traversal now 47 | self.assertTrue(isinstance(gV, GraphTraversal)) 48 | # convert it to a list to get the actual vertices 49 | vList = gV.to_list() 50 | # there should be 6 vertices 51 | self.assertEqual(6, len(vList)) 52 | # the default string representation of a vertex is showing the id 53 | # of a vertex 54 | vListStr = self.log(vList) 55 | expected = f"[v[{self.id1}], v[{self.id1+1}], v[{self.id1+2}], v[{self.id1+3}], v[{self.id1+4}], v[{self.id1+5}]]" 56 | self.assertEqual(vListStr, expected) 57 | 58 | def test_tutorial2(self): 59 | """ 60 | gremlin> g.V(1) //(2) 61 | ==>v[1] 62 | """ 63 | vListStr = self.log(self.g.V(self.id1).to_list()) 64 | expected = f"[v[{self.id1}]]" 65 | self.assertEqual(vListStr, expected) 66 | 67 | def test_tutorial3(self): 68 | """ 69 | gremlin> g.V(1).values('name') //3 70 | ==>marko 71 | """ 72 | vListStr = self.log(self.g.V(self.id1).values("name").toList()) 73 | expected = "['marko']" 74 | self.assertEqual(vListStr, expected) 75 | 76 | def test_tutorial4(self): 77 | """ 78 | gremlin> g.V(1).outE('knows') //4 79 | ==>e[7][1-knows->2] 80 | ==>e[8][1-knows->4] 81 | """ 82 | vList = self.g.V(self.id1).outE("knows").to_list() 83 | vListStr = self.log(vList) 84 | if self.remote_traversal.server.name == "Neo4j": 85 | self.assertEqual(2, len(vList)) 86 | else: 87 | expected = "[e[7][1-knows->2], e[8][1-knows->4]]" 88 | self.assertEqual(vListStr, expected) 89 | 90 | def test_tutorial5(self): 91 | """ 92 | gremlin> g.V(1).outE('knows').inV().values('name') //5\ 93 | ==>vadas 94 | ==>josh 95 | """ 96 | vList = self.g.V(self.id1).outE("knows").inV().values("name").to_list() 97 | vListStr = self.log(vList) 98 | self.assertTrue( 99 | vListStr == "['vadas', 'josh']" or vListStr == "['josh', 'vadas']" 100 | ) 101 | 102 | def test_tutorial6(self): 103 | """ 104 | gremlin> g.V(1).out('knows').values('name') //6\ 105 | ==>vadas 106 | ==>josh 107 | """ 108 | vList = self.g.V(self.id1).out("knows").values("name").to_list() 109 | vListStr = self.log(vList) 110 | self.assertTrue( 111 | vListStr == "['vadas', 'josh']" or vListStr == "['josh', 'vadas']" 112 | ) 113 | -------------------------------------------------------------------------------- /tests/jupyter.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "code", 5 | "execution_count": null, 6 | "metadata": {}, 7 | "outputs": [], 8 | "source": [ 9 | "from gremlin.remote import RemoteTraversal, Server\n", 10 | "server = Server()\n", 11 | "remote_traversal = RemoteTraversal(server=server, in_jupyter=True)\n", 12 | "g = remote_traversal.g()" 13 | ] 14 | }, 15 | { 16 | "cell_type": "code", 17 | "execution_count": null, 18 | "metadata": {}, 19 | "outputs": [], 20 | "source": [ 21 | "from gremlin.examples import Examples, Volume\n", 22 | "examples = Examples(Volume.local())\n", 23 | "examples.load_by_name(g, 'tinkerpop-modern')" 24 | ] 25 | }, 26 | { 27 | "cell_type": "code", 28 | "execution_count": null, 29 | "metadata": {}, 30 | "outputs": [], 31 | "source": [ 32 | "from gremlin.draw import GremlinDraw\n", 33 | "GremlinDraw.show(g)" 34 | ] 35 | }, 36 | { 37 | "cell_type": "code", 38 | "execution_count": null, 39 | "metadata": {}, 40 | "outputs": [], 41 | "source": [ 42 | "GremlinDraw.show_graph_traversal(g, g.V().hasLabel('person').out('knows').out('created'))" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": null, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "GremlinDraw.show_graph_traversal(g, g.V().hasLabel('person').out('knows').out('created').element_map())" 52 | ] 53 | }, 54 | { 55 | "cell_type": "code", 56 | "execution_count": null, 57 | "metadata": {}, 58 | "outputs": [], 59 | "source": [ 60 | "GremlinDraw.show_graph_traversal(g, g.E())" 61 | ] 62 | }, 63 | { 64 | "cell_type": "code", 65 | "execution_count": null, 66 | "metadata": {}, 67 | "outputs": [], 68 | "source": [ 69 | "GremlinDraw.show_graph_traversal(g, g.E().path())" 70 | ] 71 | }, 72 | { 73 | "cell_type": "code", 74 | "execution_count": null, 75 | "metadata": {}, 76 | "outputs": [], 77 | "source": [ 78 | "GremlinDraw.show_graph_traversal(g, g.V().hasLabel('person').out('knows').out('created').path())" 79 | ] 80 | }, 81 | { 82 | "cell_type": "code", 83 | "execution_count": null, 84 | "metadata": {}, 85 | "outputs": [], 86 | "source": [ 87 | "x = g.V().bothE()\n", 88 | "marko = g.addV(\"person\").next()\n", 89 | "vadas = g.addV(\"person\").next()\n", 90 | "GremlinDraw.show_graph_traversal(g, g.V(vadas).addE('knows').to(marko).property(\"weight\", 0.5).to_list())" 91 | ] 92 | }, 93 | { 94 | "cell_type": "code", 95 | "execution_count": null, 96 | "metadata": {}, 97 | "outputs": [], 98 | "source": [ 99 | "GremlinDraw.show(g)" 100 | ] 101 | }, 102 | { 103 | "cell_type": "code", 104 | "execution_count": null, 105 | "metadata": {}, 106 | "outputs": [], 107 | "source": [ 108 | "GremlinDraw.show_graph_traversal(g, g.E().hasLabel(\"knows\").toList())" 109 | ] 110 | }, 111 | { 112 | "cell_type": "code", 113 | "execution_count": null, 114 | "metadata": {}, 115 | "outputs": [], 116 | "source": [ 117 | "GremlinDraw.show_graph_traversal(g, g.V().has(\"lang\").toList())" 118 | ] 119 | }, 120 | { 121 | "cell_type": "code", 122 | "execution_count": null, 123 | "metadata": {}, 124 | "outputs": [], 125 | "source": [ 126 | "from gremlin_python.process.graph_traversal import __\n", 127 | "GremlinDraw.show_graph_traversal(g, g.V().or_(__.has(\"lang\"), __.not_(__.has(\"age\"))).toList())" 128 | ] 129 | }, 130 | { 131 | "cell_type": "code", 132 | "execution_count": null, 133 | "metadata": {}, 134 | "outputs": [], 135 | "source": [ 136 | "GremlinDraw.show_graph_traversal(g, g.V().hasLabel(\"person\").limit(3).toList())" 137 | ] 138 | }, 139 | { 140 | "cell_type": "code", 141 | "execution_count": null, 142 | "metadata": {}, 143 | "outputs": [], 144 | "source": [ 145 | "GremlinDraw.show_graph_traversal(g, g.V().has(\"name\", \"peter\").outE().toList())" 146 | ] 147 | }, 148 | { 149 | "cell_type": "code", 150 | "execution_count": null, 151 | "metadata": {}, 152 | "outputs": [], 153 | "source": [ 154 | "GremlinDraw.show_graph_traversal(g, g.V(4).bothE().toList())" 155 | ] 156 | }, 157 | { 158 | "cell_type": "code", 159 | "execution_count": null, 160 | "metadata": {}, 161 | "outputs": [], 162 | "source": [ 163 | "GremlinDraw.show_graph_traversal(g, g.E(\"11\").bothV().toList())" 164 | ] 165 | }, 166 | { 167 | "cell_type": "code", 168 | "execution_count": null, 169 | "metadata": {}, 170 | "outputs": [], 171 | "source": [ 172 | "GremlinDraw.show_graph_traversal(g, g.V().hasLabel('person').outE().otherV().toList())" 173 | ] 174 | }, 175 | { 176 | "cell_type": "code", 177 | "execution_count": null, 178 | "metadata": {}, 179 | "outputs": [], 180 | "source": [ 181 | "GremlinDraw.show_graph_traversal(g, g.V(3).repeat(__.both()).times(2).path().toList())" 182 | ] 183 | } 184 | ], 185 | "metadata": { 186 | "kernelspec": { 187 | "display_name": "dbis-hiwi", 188 | "language": "python", 189 | "name": "python3" 190 | }, 191 | "language_info": { 192 | "codemirror_mode": { 193 | "name": "ipython", 194 | "version": 3 195 | }, 196 | "file_extension": ".py", 197 | "mimetype": "text/x-python", 198 | "name": "python", 199 | "nbconvert_exporter": "python", 200 | "pygments_lexer": "ipython3", 201 | "version": "3.11.3" 202 | }, 203 | "orig_nbformat": 4 204 | }, 205 | "nbformat": 4, 206 | "nbformat_minor": 2 207 | } 208 | -------------------------------------------------------------------------------- /gremlin/examples.py: -------------------------------------------------------------------------------- 1 | import os 2 | import urllib.request 3 | from dataclasses import dataclass 4 | from os.path import abspath, dirname 5 | from pathlib import Path 6 | 7 | from gremlin_python.process.anonymous_traversal import GraphTraversalSource 8 | 9 | from gremlin.remote import RemoteTraversal 10 | 11 | 12 | @dataclass 13 | class Volume: 14 | """ 15 | map a local path on the client to a remote 16 | path on a server e.g. when using a Volume in a docker 17 | container 18 | """ 19 | 20 | local_path: str 21 | remote_path: str 22 | 23 | def local(self, file_name: str): 24 | """ 25 | return the local mapping of the given file_name 26 | 27 | Args: 28 | file_name(str): the file name to map 29 | Returns: 30 | str: the local path 31 | """ 32 | path = f"{self.local_path}/{file_name}" 33 | return path 34 | 35 | def remote(self, file_name: str): 36 | """ 37 | return the remote mapping of the given file_name 38 | 39 | Args: 40 | file_name(str): the file name to map 41 | Returns: 42 | str: the remote path 43 | """ 44 | path = f"{self.remote_path}/{file_name}" 45 | return path 46 | 47 | @staticmethod 48 | def docker() -> "Volume": 49 | """ 50 | get the default docker volume mapping 51 | 52 | Returns: 53 | Volume: the local_path/remote_path mapping 54 | """ 55 | home = str(Path.home()) 56 | local_path = f"{home}/.gremlin-examples" 57 | os.makedirs(local_path, exist_ok=True) 58 | remote_path = "/opt/gremlin-server/data/examples" 59 | volume = Volume(local_path=local_path, remote_path=remote_path) 60 | return volume 61 | 62 | @staticmethod 63 | def local() -> "Volume": 64 | """ 65 | get the default local volume mapping 66 | 67 | Returns: 68 | Volume: the local_path/remote_path mapping 69 | """ 70 | home = str(Path.home()) 71 | local_path = f"{home}/.gremlin-examples" 72 | os.makedirs(local_path, exist_ok=True) 73 | remote_path = str(abspath(f"{dirname(abspath(__file__))}/data")) 74 | volume = Volume(local_path=local_path, remote_path=remote_path) 75 | return volume 76 | 77 | 78 | @dataclass 79 | class Example: 80 | name: str 81 | url: str 82 | 83 | def load( 84 | self, 85 | g: GraphTraversalSource, 86 | volume: Volume, 87 | force: bool = False, 88 | debug: bool = False, 89 | ) -> None: 90 | """ 91 | download graph from remote_path to local_path depending on force flag 92 | and load graph into g 93 | 94 | Args: 95 | g(GraphTraversalSource): the target graph (inout) 96 | volume:Volume 97 | force(bool): if True download even if local copy already exists 98 | debug(bool): if True show debugging information 99 | """ 100 | self.download(volume.local_path, force=force, debug=debug) 101 | graph_xml = f"{volume.remote_path}/{self.name}.xml" 102 | RemoteTraversal.load(g, graph_xml) 103 | 104 | def download(self, path, force: bool = False, debug: bool = False) -> str: 105 | """ 106 | load the graphml xml file from the given url and store it to the given file_name (prefix) 107 | 108 | Args: 109 | url(str): the url to use 110 | file_name(str): the name of the file to load 111 | force(bool): if True overwrite 112 | debug(bool): if True show debugging information 113 | 114 | Returns: 115 | str: the filename loaded 116 | """ 117 | graph_xml = f"{path}/{self.name}.xml" 118 | # check whether file exists and is size 0 which indicates 119 | # a failed download attempt 120 | if os.path.exists(graph_xml): 121 | stats = os.stat(graph_xml) 122 | size = stats.st_size 123 | force = force or size == 0 124 | if debug: 125 | print(f"{graph_xml}(size {size}) already downloaded ...") 126 | if not os.path.exists(graph_xml) or force: 127 | if debug: 128 | print(f"downloading {self.url} to {graph_xml} ...") 129 | graph_data = urllib.request.urlopen(self.url).read().decode("utf-8") 130 | print(graph_data, file=open(graph_xml, "w")) 131 | return graph_xml 132 | 133 | 134 | class Examples: 135 | """ 136 | Examples 137 | """ 138 | 139 | def __init__(self, volume: Volume, debug: bool = False): 140 | """ 141 | Constructor 142 | 143 | Args: 144 | volume:Volume 145 | debug(bool): if true switch on debugging 146 | 147 | """ 148 | self.debug = debug 149 | self.volume = volume 150 | self.examples_by_name = {} 151 | for example in [ 152 | Example( 153 | name="tinkerpop-modern", 154 | url="https://raw.githubusercontent.com/apache/tinkerpop/master/data/tinkerpop-modern.xml", 155 | ), 156 | Example( 157 | name="grateful-dead", 158 | url="https://raw.githubusercontent.com/apache/tinkerpop/master/data/grateful-dead.xml", 159 | ), 160 | Example( 161 | name="air-routes-small", 162 | url="https://raw.githubusercontent.com/krlawrence/graph/master/sample-data/air-routes-small.graphml", 163 | ), 164 | Example( 165 | name="air-routes-latest", 166 | url="https://raw.githubusercontent.com/krlawrence/graph/master/sample-data/air-routes-latest.graphml", 167 | ), 168 | ]: 169 | self.examples_by_name[example.name] = example 170 | 171 | def load_by_name(self, g: GraphTraversalSource, name: str) -> None: 172 | """ 173 | load an example by name to the given graph 174 | 175 | Args: 176 | g(GraphTraversalSource): the target graph (inout) 177 | name(str): the name of the example 178 | 179 | Raises: 180 | Exception: if the example does not exist 181 | """ 182 | if name in self.examples_by_name: 183 | example = self.examples_by_name[name] 184 | example.load(g, self.volume, debug=self.debug) 185 | else: 186 | raise Exception(f"invalid example {name}") 187 | -------------------------------------------------------------------------------- /gremlin/remote.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2019-09-17 3 | 4 | @author: wf 5 | """ 6 | 7 | from __future__ import annotations 8 | 9 | import io 10 | import os.path 11 | import socket 12 | from contextlib import closing 13 | from os.path import abspath, dirname 14 | from typing import Optional 15 | 16 | import yaml 17 | from gremlin_python.driver.aiohttp.transport import AiohttpTransport 18 | from gremlin_python.driver.driver_remote_connection import DriverRemoteConnection 19 | from gremlin_python.process.anonymous_traversal import GraphTraversalSource, traversal 20 | 21 | 22 | class RemoteTraversal: 23 | """ 24 | helper class for Apache Tinkerpop Gremlin Python GLV remote access 25 | """ 26 | 27 | def __init__(self, server: Server, in_jupyter: bool = False) -> None: 28 | """ 29 | constructor 30 | 31 | """ 32 | self.server = server 33 | self.in_jupyter = in_jupyter 34 | 35 | @staticmethod 36 | def fromYaml( 37 | serverName="server", config_path: Optional[str] = None, in_jupyter: bool = False 38 | ) -> "RemoteTraversal": 39 | """ 40 | create a server from the given yaml file 41 | 42 | Args: 43 | serverName(str): the servername to use 44 | config_path(str): the path to the server configuration file 45 | """ 46 | server = Server.read(serverName, config_path) 47 | rt = RemoteTraversal(server, in_jupyter=in_jupyter) 48 | return rt 49 | 50 | def g(self) -> GraphTraversalSource: 51 | """ 52 | get the graph traversal source 53 | 54 | Returns: 55 | the graph traversal source 56 | """ 57 | server = self.server 58 | url = f"ws://{server.host}:{server.port}/gremlin" 59 | # https://github.com/orientechnologies/orientdb-gremlin/issues/143 60 | # username="root" 61 | # password="rootpwd" 62 | if self.in_jupyter: 63 | self.remoteConnection = DriverRemoteConnection( 64 | url, 65 | server.alias, 66 | username=server.username, 67 | password=server.password, 68 | transport_factory=lambda: AiohttpTransport(call_from_event_loop=True), 69 | ) 70 | else: 71 | self.remoteConnection = DriverRemoteConnection( 72 | url, server.alias, username=server.username, password=server.password 73 | ) 74 | g = traversal().withRemote(self.remoteConnection) 75 | return g 76 | 77 | def close(self) -> None: 78 | """ 79 | close my connection 80 | """ 81 | self.remoteConnection.close() 82 | 83 | @staticmethod 84 | def load(g: GraphTraversalSource, graphmlFile) -> None: 85 | """ 86 | load the given graph from the given graphmlFile 87 | """ 88 | # make the local file accessible to the server 89 | xmlPath = os.path.abspath(graphmlFile) 90 | # drop the existing content of the graph 91 | g.V().drop().iterate() 92 | # read the content from the graphmlFile 93 | g.io(xmlPath).read().iterate() 94 | 95 | @staticmethod 96 | def clear(g: GraphTraversalSource) -> None: 97 | """ 98 | clear the given graph 99 | """ 100 | g.V().drop().iterate() 101 | 102 | 103 | class Server: 104 | """ 105 | Server description 106 | """ 107 | 108 | debug = False 109 | 110 | # construct me with the given alias 111 | def __init__( 112 | self, 113 | host: str = "localhost", 114 | port: int = 8182, 115 | alias: str = "g", 116 | name: str = "TinkerGraph", 117 | username: str = "", 118 | password: str = "", 119 | debug: bool = False, 120 | helpUrl: str = "http://wiki.bitplan.com/index.php/Gremlin_python#Connecting_to_Gremlin_enabled_graph_databases", 121 | ) -> None: 122 | """ 123 | constructor 124 | 125 | Args: 126 | host(str): the host to connect to 127 | port(int): the port to connect to 128 | alias(str): the alias to use 129 | name(str): the name of the server 130 | username(Optional[str]): the username to use 131 | password(Optional[str]): the password to use 132 | debug(bool): True if debug output should be generated 133 | helpUrl(str): the help url to use 134 | """ 135 | self.host = host 136 | self.port = port 137 | self.alias = alias 138 | self.name = name 139 | self.username = username 140 | self.password = password 141 | Server.debug = debug 142 | self.helpUrl = helpUrl 143 | 144 | def check_socket(self) -> bool: 145 | """ 146 | check my socket 147 | 148 | Returns: 149 | True if socket is open 150 | """ 151 | with closing(socket.socket(socket.AF_INET, socket.SOCK_STREAM)) as sock: 152 | is_open = sock.connect_ex((self.host, self.port)) == 0 153 | return is_open 154 | 155 | # return a readable representation of me 156 | def __repr__(self) -> str: 157 | return "%s(%r)" % (self.__class__, self.__dict__) 158 | 159 | @staticmethod 160 | def read(name: str, config_path: Optional[str] = None) -> "Server": 161 | """ 162 | read me from a yaml file 163 | 164 | Args: 165 | name(str): the name of the server 166 | config_path(str): the path to the config files 167 | 168 | Returns: 169 | the server 170 | 171 | Raises: 172 | Exception: if the yaml file is missing 173 | """ 174 | if config_path is None: 175 | script_path = dirname(abspath(__file__)) 176 | config_path = abspath(f"{script_path}/config") 177 | yamlFile = f"{config_path}/{name}.yaml" 178 | # is there a yamlFile for the given name 179 | if os.path.isfile(yamlFile): 180 | with io.open(yamlFile, "r") as stream: 181 | if Server.debug: 182 | print("reading %s" % (yamlFile)) 183 | server = yaml.load(stream, Loader=yaml.Loader) 184 | if Server.debug: 185 | print(server) 186 | return server 187 | else: 188 | raise Exception(f"{yamlFile} is missing") 189 | 190 | # write me to my yaml file 191 | def write(self) -> None: 192 | """ 193 | write me to my yaml file 194 | """ 195 | yamlFile = self.name + ".yaml" 196 | with io.open(yamlFile, "w", encoding="utf-8") as stream: 197 | yaml.dump(self, stream) 198 | if Server.debug: 199 | print(yaml.dump(self)) 200 | -------------------------------------------------------------------------------- /scripts/run: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # WF 2019-09-17 3 | # improved WF 2023-05-26 4 | # test gremlin-python 5 | 6 | # see https://stackoverflow.com/questions/57936915/how-do-i-get-gremlin-python-with-gremlin-server-3-4-3-to-work 7 | version=3.7.3 8 | mirror=https://dlcdn.apache.org/tinkerpop/$version 9 | gsd=apache-tinkerpop-gremlin-server-${version} 10 | gcd=apache-tinkerpop-gremlin-console-${version} 11 | # python and pip commands to be used 12 | # on macports see e.g. http://johnlaudun.org/20150512-installing-and-setting-pip-with-macports/ for how to modify these 13 | # e.g. with sudo port select --set pip3 pip37 14 | pip=pip3 15 | python=python3 16 | pyversion=3.9 17 | 18 | if [ "$USER" = "travis" ] 19 | then 20 | pip=pip 21 | python=python 22 | fi 23 | 24 | #ansi colors 25 | #http://www.csc.uvic.ca/~sae/seng265/fall04/tips/s265s047-tips/bash-using-colors.html 26 | blue='\033[0;34m' 27 | red='\033[0;31m' 28 | green='\033[0;32m' # '\e[1;32m' is too bright for white bg. 29 | endColor='\033[0m' 30 | 31 | # 32 | # a colored message 33 | # params: 34 | # 1: l_color - the color of the message 35 | # 2: l_msg - the message to display 36 | # 37 | color_msg() { 38 | local l_color="$1" 39 | local l_msg="$2" 40 | echo -e "${l_color}$l_msg${endColor}" 41 | } 42 | 43 | # error 44 | # 45 | # show an error message and exit 46 | # 47 | # params: 48 | # 1: l_msg - the message to display 49 | error() { 50 | local l_msg="$1" 51 | # use ansi red for error 52 | color_msg $red "Error: $l_msg" 1>&2 53 | } 54 | 55 | # 56 | # show the usage 57 | # 58 | usage() { 59 | echo "usage: $0 [-c|-h|-i|-n|-p|-s|-t|-v]" 60 | echo " -c|--console: start groovy console" 61 | echo " -cd|--console_docker: start groovy console via docker" 62 | echo " -h|--help: show this usage" 63 | echo " -i|--install: install prerequisites" 64 | echo " -n|--neo4j: start neo4j server" 65 | echo " -p|--python: start python trial code" 66 | echo " -s|--server: start server" 67 | echo " -sd|--server_server: start server via docker" 68 | echo " -t|--test: start pytest" 69 | echo " -v|--version: show version" 70 | exit 1 71 | } 72 | 73 | # 74 | # checkinstalled 75 | # 76 | # check that l_prog is available by calling which 77 | # if not available install from given package depending on Operating system 78 | # 79 | # params: 80 | # 1: l_prog: The program that shall be checked 81 | # 2: l_version: The option to check the version of the program 82 | # 3: l_linuxpackage: The apt-package to install from 83 | # 4: l_macospackage: The MacPorts package to install from 84 | # 85 | checkinstalled() { 86 | local l_prog=$1 87 | local l_version="$2" 88 | local l_linuxpackage=$3 89 | local l_macospackage=$4 90 | os=`uname` 91 | color_msg $green "checking that $l_prog is installed on os $os ..." 92 | which $l_prog 93 | if [ $? -eq 0 ] 94 | then 95 | $l_prog $l_version 96 | else 97 | case $os in 98 | # Mac OS 99 | Darwin) l_package=$l_macospackage;; 100 | *) l_package=$l_linuxpackage;; 101 | esac 102 | color_msg $blue "$l_prog is not available - shall i install it from $l_package y/n/a?" 103 | read x 104 | case $x in 105 | y) 106 | case $os in 107 | # Mac OS 108 | Darwin) 109 | color_msg $blue "installing $l_prog from MacPorts package $l_macospackage" 110 | sudo port install $l_macospackage 111 | ;; 112 | # e.g. Ubuntu/Fedora/Debian/Suse 113 | Linux) 114 | color_msg $blue "installing $l_prog from apt-package $l_linuxpackage" 115 | sudo apt-get install -y $l_linuxpackage 116 | ;; 117 | # git bash (Windows) 118 | MINGW32_NT-6.1) 119 | error "$l_prog ist not installed" 120 | ;; 121 | *) 122 | error "unknown operating system $os" 123 | esac;; 124 | a) 125 | color_msg $red "aborting ..." 126 | exit 1;; 127 | esac 128 | fi 129 | } 130 | 131 | # 132 | # get the realpath for the given path 133 | # 134 | getrealpath() { 135 | local l_path="$1" 136 | case $(uname) in 137 | Darwin) 138 | echo $(pwd)/$l_path 139 | ;; 140 | *) 141 | realpath $l_path 142 | ;; 143 | esac 144 | } 145 | 146 | # install prerequisites 147 | install() { 148 | local l_pyversion="$1" 149 | local l_pyversionNumeric=$(echo $1 | sed "s/\.//g") 150 | color_msg $blue "checking prerequisites ..." 151 | checkinstalled java "-version" "openjdk-8-jre" "openjdk8" 152 | checkinstalled $python "--version" "python${l_pyversion}" "python${l_pyversionNumeric}" 153 | checkinstalled $pip "--version" "python${l_pyversion}-pip" "py{$l_pyversionNumeric}-pip" 154 | checkinstalled pytest "--version" "python-pytest" "py${l_pyversionNumeric}-pytest" 155 | 156 | for d in $gsd $gcd 157 | do 158 | if [ ! -d $d ] 159 | then 160 | zip=$d-bin.zip 161 | if [ ! -f $zip ] 162 | then 163 | color_msg $blue "downloading $zip" 164 | curl -s $mirror/$zip -o $zip 165 | else 166 | color_msg $green "$zip already downloaded" 167 | fi 168 | color_msg $blue "unzipping $zip" 169 | unzip -q $zip 170 | else 171 | color_msg $green "$d already unzipped" 172 | fi 173 | done 174 | color_msg $blue "installing needed python modules" 175 | pip install . 176 | } 177 | 178 | # commandline option 179 | while [ "$1" != "" ] 180 | do 181 | option=$1 182 | shift 183 | case $option in 184 | -i|--install) 185 | install $pyversion;; 186 | -s|--server) 187 | #conf=$(realpath $gsd/conf/gremlin-server-modern-py.yaml) 188 | conf=$(getrealpath $gsd/conf/gremlin-server-modern.yaml) 189 | color_msg $blue "starting gremlin-server ... using $conf" 190 | $gsd/bin/gremlin-server.sh $conf 191 | ;; 192 | -sd|--server_docker) 193 | example_dir=$HOME/.gremlin-examples 194 | if [ ! -d $example_dir ] 195 | then 196 | color_msg $blue "create example directory $example_dir ..." 197 | mkdir -p $example_dir 198 | else 199 | color_msg $green "example directory $example_dir already exists ..." 200 | fi 201 | 202 | color_msg $blue "starting gremlin-server via Docker ..." 203 | # export GREMLIN_YAML=/opt/gremlin-server/conf/gremlin-server.yaml 204 | docker run --name gremlin-server -v $example_dir:/opt/gremlin-server/data/examples -p 8182:8182 tinkerpop/gremlin-server:$version conf/gremlin-server.yaml 205 | # 206 | ;; 207 | -n|--neo4j) 208 | plugin=neo4j-gremlin 209 | if [ ! -d $gsd/ext/$plugin ] 210 | then 211 | color_msg $blue "installing plugin $plugin" 212 | $gsd/bin/gremlin-server.sh install org.apache.tinkerpop $plugin $version 213 | else 214 | color_msg $green "$plugin plugin already installed" 215 | fi 216 | color_msg $blue "starting neo4j gremlin-server ..." 217 | conf=$(realpath $gsd/conf/gremlin-server-neo4j.yaml) 218 | $gsd/bin/gremlin-server.sh $conf 219 | ;; 220 | -b|--bash) 221 | color_msg $blue "starting docker bash" 222 | docker exec -it gremlin-server /bin/bash 223 | ;; 224 | -c|--console) 225 | color_msg $blue "starting gremlin-console ..." 226 | $gcd/bin/gremlin.sh 227 | ;; 228 | -cd|--console_docker) 229 | color_msg $blue "starting gremlin-console via Docker..." 230 | docker run -it tinkerpop/gremlin-console:$version --name gremlin-console 231 | ;; 232 | -p|--python) 233 | color_msg $blue "starting python test code" 234 | $python -m unittest tests/test_tutorial.py 235 | ;; 236 | -pv|--pythonversion) 237 | shift 238 | pyversion="$1" 239 | color_msg $blue "using python version $pyversion" 240 | ;; 241 | -v|--version) 242 | color_msg $blue "apache-tinkerpop-gremlin version $version" 243 | ;; 244 | -t|--test) 245 | $python -m pytest -p no:warnings -s 246 | ;; 247 | -h|--help) 248 | usage;; 249 | *) 250 | error "invalid option $option" 251 | usage 252 | esac 253 | done 254 | -------------------------------------------------------------------------------- /gremlin/draw.py: -------------------------------------------------------------------------------- 1 | """ 2 | Created on 2023-05-15 3 | 4 | @author: jv 5 | """ 6 | 7 | from collections.abc import Iterable 8 | from dataclasses import dataclass 9 | from typing import Any, List, Union 10 | 11 | import graphviz 12 | from aenum import Enum 13 | from gremlin_python.process.anonymous_traversal import GraphTraversalSource 14 | from gremlin_python.process.graph_traversal import GraphTraversal 15 | from gremlin_python.process.traversal import T 16 | from gremlin_python.structure.graph import Edge, Path, Vertex 17 | 18 | 19 | @dataclass 20 | class GremlinDrawConfig: 21 | """ 22 | draw configuration parameters 23 | """ 24 | 25 | fontname: str = "arial" 26 | fillcolor: str = "#ADE1FE" 27 | output_format: str = "pdf" 28 | edge_line_width: int = 3 29 | dash_width: int = 5 # number of dashes to apply 30 | v_limit: int = 10 # maximum number of vertices to show 31 | e_limit: int = 10 # maximum number of edges to show 32 | # optionally set the properties to be displayed 33 | vertex_properties: List[str] = None # New filter for vertex properties 34 | edge_properties: List[str] = None # New filter for edge properties 35 | 36 | 37 | class GremlinDraw: 38 | """ 39 | helper class to draw Gremlin Graphs via Graphviz 40 | """ 41 | 42 | def __init__( 43 | self, g: GraphTraversalSource, title: str, config: GremlinDrawConfig = None 44 | ): 45 | """ 46 | constructor 47 | """ 48 | self.g = g 49 | self.title = title 50 | if config is None: 51 | config = GremlinDrawConfig() 52 | self.config = config 53 | self.gviz: graphviz.Digraph = graphviz.Digraph( 54 | title, format=config.output_format 55 | ) 56 | # keep track of the vertices and edges drawn 57 | self.v_drawn = {} 58 | self.e_drawn = {} 59 | 60 | def __as_label(self, head, body: str) -> str: 61 | """ 62 | create a label from head and body separated by a dash 63 | with the configured width 64 | """ 65 | # note the UTF-8 dash ... 66 | dash = "─" * self.config.dash_width 67 | label = f"{head}\n{dash}\n{body}" 68 | return label 69 | 70 | def get_vertex_properties(self, vertex: Vertex) -> list: 71 | """ 72 | get the properties for a given vertex 73 | """ 74 | # developer note: see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/python/gremlin_python/structure/graph.py#LL58C23-L58C23 75 | # has properties but these are not set as for gremlin-python 3.7.0 76 | 77 | # get the properties of the vertex (work around) 78 | kvp_list = list(next(self.g.V(vertex).element_map()).items()) 79 | # non-property items are of type aenum 80 | properties = [item for item in kvp_list if not isinstance(item[0], Enum)] 81 | assert len(properties) == len(kvp_list) - 2 # ID and label are not properties 82 | if self.config.vertex_properties is not None: 83 | properties = [ 84 | item for item in properties if item[0] in self.config.vertex_properties 85 | ] 86 | return properties 87 | 88 | def get_edge_properties(self, edge: Edge) -> list: 89 | # developer note: see https://github.com/apache/tinkerpop/blob/master/gremlin-python/src/main/python/gremlin_python/structure/graph.py#L66 90 | # when gremlin-python 3.7.0 is released, the following code might be improved (get the properties using edge.properties) 91 | # e_props=edge.properties 92 | # 2023-08-21: WF tested - but properties are not set ... 93 | # then, g can also be removed as a parameter 94 | # get the properties of the edge 95 | edge_t = self.g.E(edge) 96 | try: 97 | edge_map = edge_t.element_map().next() 98 | kvp_list = list(edge_map.items()) 99 | except StopIteration: 100 | pass 101 | return [] 102 | 103 | # Workaround, because the above line does not work due to inconsistencies / bugs in the gremlin-python library 104 | # kvp_list = [edge_element_map for edge_element_map in self.g.E().element_map().to_list() if edge_element_map[T.id] == edge.id][0].items() 105 | # non-property items are of type aenum 106 | properties = [item for item in kvp_list if not isinstance(item[0], Enum)] 107 | assert ( 108 | len(properties) == len(kvp_list) - 4 109 | ) # ID, label, in, and out are not properties 110 | if self.config.edge_properties is not None: 111 | properties = [ 112 | item for item in properties if item[0] in self.config.edge_properties 113 | ] 114 | return properties 115 | 116 | def draw_vertex(self, vertex: Vertex): 117 | """ 118 | draw a single given vertex 119 | """ 120 | # avoid drawing to many vertices 121 | if len(self.v_drawn) >= self.config.v_limit: 122 | return 123 | if vertex.id in self.v_drawn: 124 | return 125 | properties = self.get_vertex_properties(vertex) 126 | properties_label = "\n".join(f"{key}: {value}" for key, value in properties) 127 | head = f"{str(vertex.id)}\n{vertex.label}" 128 | body = f"{properties_label}" 129 | label = self.__as_label(head, body) 130 | # draw the vertex 131 | self.gviz.node( 132 | name=str(vertex.id), 133 | label=f"{label}", 134 | fillcolor=f"{self.config.fillcolor}", 135 | style="filled", 136 | fontname=f"{self.config.fontname}", 137 | ) 138 | self.v_drawn[vertex.id] = vertex 139 | 140 | def draw_edge(self, edge: Edge, with_vertices: bool = True): 141 | """ 142 | draw a single given edge 143 | """ 144 | # avoid drawing to many vertices 145 | if len(self.e_drawn) >= self.config.e_limit: 146 | return 147 | if edge.id in self.e_drawn: 148 | return 149 | if with_vertices: 150 | self.draw_vertex(edge.inV) 151 | self.draw_vertex(edge.outV) 152 | pass 153 | properties = self.get_edge_properties(edge) 154 | properties_label = "\n".join(f"{key}: {value}" for key, value in properties) 155 | head = f"{str(edge.id)}\n{edge.label}" 156 | body = properties_label 157 | label = self.__as_label(head, body) 158 | # get the image of the edge by id 159 | in_vertex_id = edge.inV.id 160 | out_vertex_id = edge.outV.id 161 | 162 | # draw the edge 163 | self.gviz.edge( 164 | tail_name=str(out_vertex_id), 165 | head_name=str(in_vertex_id), 166 | label=f"{label}", 167 | style=f"setlinewidth({self.config.edge_line_width})", 168 | fontname=f"{self.config.fontname}", 169 | ) 170 | self.e_drawn[edge.id] = edge 171 | 172 | def draw_g(self): 173 | # draw vertices 174 | vlist = self.g.V().to_list() 175 | vlist = vlist[: self.config.v_limit] 176 | 177 | for v in vlist: 178 | self.draw_vertex(v) 179 | 180 | # draw edges 181 | elist = self.g.E().to_list() 182 | elist = elist[: self.config.e_limit] 183 | 184 | for e in elist: 185 | self.draw_edge(e) 186 | 187 | def draw(self, gt: Union[GraphTraversal, Any]): 188 | # developer note: when moving the minimum supported version up to 3.10, the following code can be greatly improved by using match statements 189 | worklist: List[Any] = ( 190 | gt.to_list() 191 | if isinstance(gt, GraphTraversal) 192 | else list(gt) if isinstance(gt, Iterable) else [gt] 193 | ) 194 | 195 | while len(worklist) > 0: 196 | # move any vertices to the front of the worklist (draw them first) 197 | worklist = [item for item in worklist if not isinstance(item, Vertex)] + [ 198 | item for item in worklist if isinstance(item, Vertex) 199 | ] 200 | 201 | result = worklist.pop(0) 202 | 203 | if isinstance(result, Vertex): 204 | self.draw_vertex(result) 205 | elif isinstance(result, Edge): 206 | self.draw_edge(result) 207 | elif isinstance(result, Path): 208 | for item in result.objects: 209 | worklist.append(item) 210 | elif isinstance(result, dict): 211 | if T.id in result: 212 | # check if the id is a vertex or an edge 213 | if self.g.V(result[T.id]).hasNext(): 214 | self.draw_vertex(next(self.g.V(result[T.id]))) 215 | elif self.g.E(result[T.id]).hasNext(): 216 | self.draw_edge(self.g.E(result[T.id]).next()) 217 | else: 218 | # raise Exception("id not found") 219 | pass # silent skip 220 | else: 221 | # raise Exception("id not found") 222 | pass # silent skip 223 | else: 224 | # raise Exception(f"unknown type: {type(result)}") 225 | pass # silent skip 226 | 227 | @staticmethod 228 | def show( 229 | g: GraphTraversalSource, 230 | title: str = "Gremlin", 231 | v_limit: int = 10, 232 | e_limit: int = 10, 233 | ) -> graphviz.Digraph: 234 | """ 235 | draw the given graph 236 | """ 237 | gd = GremlinDraw(g=g, title=title) 238 | gd.config.v_limit = v_limit 239 | gd.config.e_limit = e_limit 240 | gd.draw_g() 241 | return gd.gviz 242 | 243 | @staticmethod 244 | def show_graph_traversal( 245 | g: GraphTraversalSource, gt: Union[GraphTraversal, Any], title: str = "Gremlin" 246 | ) -> graphviz.Digraph: 247 | """ 248 | draw the given graph traversal 249 | """ 250 | gd = GremlinDraw(g=g, title=title) 251 | gd.draw(gt) 252 | return gd.gviz 253 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------