├── 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 | [](https://pypi.org/project/gremlin-python-tutorial/)
2 | [](https://pypi.python.org/pypi/gremlin-python-tutorial/)
3 | [](https://github.com/WolfgangFahl/gremlin-python-tutorial/actions/workflows/build.yml)
4 | [](https://github.com/WolfgangFahl/gremlin-python-tutorial/issues)
5 | [](https://github.com/WolfgangFahl/gremlin-python-tutorial/issues/?q=is%3Aissue+is%3Aclosed)
6 | [](https://WolfgangFahl.github.io/gremlin-python-tutorial/)
7 | [](https://www.apache.org/licenses/LICENSE-2.0)
8 | [
](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 |
--------------------------------------------------------------------------------