diff --git a/README.md b/README.md index da2cd9d..89fe944 100644 --- a/README.md +++ b/README.md @@ -1,284 +1,10 @@ -# TypeDB Jupyter connector - -Runs TypeQL statements against a TypeDB database from a Jupyter notebook using the `%typedb` and `%typeql` IPython magic -commands. Includes: -- Full support for TypeDB Core and Cluster. -- Ability to manage multiple concurrent connections. -- Automatic session and transaction handling. -- JSON-style output for all read queries. -- Variable interpolation from the Jupyter namespace. -- Query reading from supplied filepaths. - -## Getting started - -Install this module with: - -``` -pip install typedb-jupyter -``` - -or your environment equivalent. Load the extension in Jupyter with: - -``` -%load_ext typedb_jupyter -``` - -## Connecting to TypeDB - -Establish a connection with: - -``` -%typedb -d [-a ] [-n ] -``` - -for example: - -``` -In [1]: %typedb -a 111.111.111.111:1729 -d database_1 - -Out[1]: Opened connection: database_1@111.111.111.111:1729 -``` - - -``` -In [2]: %typedb -a 222.222.222.222:1729 -d database_2 -n test_connection - -Out[2]: Opened connection: test_connection (database_2@222.222.222.222:1729) -``` - - -``` -In [3]: %typedb -d database_local - -Out[3]: Opened connection: database_local@localhost:1729 -``` - -If no address is provided, the default `localhost:1729` will be used. If no custom alias is provided, the connection -will be assigned a default alias of the format `@`. Custom aliases can only include -alphanumeric characters, hyphens, and underscores. If a connection with the server is established but no database with -the name provided exists, a new database will be created with that name by default. Only one connection can be opened to -each database at a time. - -For connecting to TypeDB Cluster, use: - -``` -%typedb -d -a -u -p -c [-n ] -``` - -List established connections with: - -``` -In [4]: %typedb -l - -Out[4]: Open connections: - ...: database_1@111.111.111.111:1729 - ...: test_connection (database_2@222.222.222.222:1729) - ...: * database_local@localhost:1729 -``` - -An asterisk appears next to the currently selected connection, which is the last one opened by default. To change the -selected connection, use: - -``` -%typedb -n -``` - -for example: - -``` -In [5]: %typedb -n database_1@111.111.111.111:1729 - -Out[5]: Selected connection: database_1@111.111.111.111:1729 -``` - -``` -In [6]: %typedb -n test_connection - -Out[6]: Selected connection: test_connection -``` - -Close a connection with: - -``` -%typedb -k -``` - -for example: - -``` -In [7]: %typedb -c database_2@222.222.222.222:1729 - -Out[7]: Closed connection: database_2@222.222.222.222:1729 -``` - -If the currently selected connection is closed, a new one must be manually selected before queries can be executed. -Using `-x` instead of `-k` will also delete the database. - -## Executing a query - -Run a query against a database using the selected connection with: - -``` -%typeql -``` - -or - -``` -%%typeql -``` - -For example: - -``` -In [8]: %typeql match $p isa person; - -Out[8]: [{'p': {'type': 'person'}}, - ...: {'p': {'type': 'person'}}] -``` - -``` -In [9]: %%typeql - ...: match - ...: $p isa person, - ...: has name $n, - ...: has age $a; - -Out[9]: [{'a': {'type': 'age', 'value_type': 'long', 'value': 30}, - ...: 'p': {'type': 'person'}, - ...: 'n': {'type': 'name', 'value_type': 'string', 'value': 'Kevin'}}, - ...: {'a': {'type': 'age', 'value_type': 'long', 'value': 50}, - ...: 'p': {'type': 'person'}, - ...: 'n': {'type': 'name', 'value_type': 'string', 'value': 'Gavin'}}] -``` - -Results of read queries are returned in a JSON-like native Python object. The shape of the object is dependent on the -type of query, as described in the following table: - -| Query type | Output object type | -|-------------------------|--------------------| -| `match` | `list` | -| `match-group` | `dict>` | -| `match-aggregate` | `intǀfloat` | -| `match-group-aggregate` | `dict` | - -Queries automatically interpolate variables from the notebook's Python namespace, specified using the syntax -`{}`, for example: - -``` -In [10]: age = 30 - -In [11]: %typeql match $p isa person, has name $n, has age {age}; count; - -Out[11]: 1 -``` - -Similarly, results can be saved to a namespace variable by providing the variable name with: - -``` -%typeql -r -``` - -for example: - -``` -In [12]: %typeql -r name_counts match $p isa person, has name $n, has age $a; group $n; count; - -In [13]: name_counts - -Out[13]: {'Gavin': 1, 'Kevin': 1} -``` - -To execute a query in a stored TypeQL file, supply the filepath with: - -``` -%typeql -f -``` - -Rule inference is disabled by default. It can be enabled for a query with: - -``` -%typeql -i True -``` - -In order to enable rule inference globally, see the [Configuring options](#configuring-options) -section below. - -## Information for advanced users - -Queries are syntactically analysed to automatically determine schema and transaction types, but these can be overridden -with: - -``` -%typeql [-s ] [-t ] -``` - -where `` is either `schema` or `data`, and `` is either `read` or `write`. - -When a connection is instantiated, a data session is opened and persisted for the duration of the connection unless a -schema query is issued, at which point the data session is closed and a schema session is opened. After the schema query -has been executed, the schema session is then closed and a new data session opened. Each call of `%typeql` or `%%typeql` -is executed in a new transaction, which is then immediately closed on completion. All clients, sessions, and -transactions are closed automatically when the notebook's kernel is terminated. - -It is important to note that TypeDB sessions and transactions cannot be opened under certain conditions, regardless of -the client: - -- Only one schema session can be opened at any time. -- Data write transactions cannot be opened while a schema session is open. -- Only one schema write transaction can be opened at any time. - -This means that, when a `define` or `undefine` query is executed in a notebook, this will interfere with queries -performed by other users on the same database. - -## Configuring options - -Certain options can be configured using the `%config` magic with: - -``` -%config ` -``` - -After being set, these options persist for the remainder of the notebook unless -changed again. The following table describes the available arguments: - -| Argument | Usage | Default | -|-----------------------------------------------|-------------------------------------------------------------------------------|---------| -| `TypeDBMagic` | List config options and current set values for `%typedb`. | | -| `TypeDBMagic.create_database = ` | Create database when opening a connection if it does not already exist. | `True` | -| `TypeQLMagic` | List config options and current set values for `%typeql`. | | -| `TypeQLMagic.global_inference = ` | Enable rule inference for all queries. Can be overridden per query with `-i`. | `False` | -| `TypeQLMagic.show_info = ` | Always show full connection information when executing a query. | `True` | -| `TypeQLMagic.strict_transactions = ` | Require session and transaction types to be specified for every transaction. | `False` | - -## Command glossary - -The following tables list the arguments that can be provided to the `%typedb` and `%typeql` magic commands: - -| Magic command | Argument | Usage | -|---------------|-------------------------|-----------------------------------------------------------------------------| -| `%typedb` | `-a ` | TypeDB server address for new connection. | -| `%typedb` | `-d ` | Database name for new connection. | -| `%typedb` | `-u ` | Username for new Cloud/Cluster connection. | -| `%typedb` | `-p ` | Password for new Cloud/Cluster connection. | -| `%typedb` | `-c ` | TLS certificate path for new Cloud/Cluster connection. | -| `%typedb` | `-n ` | Custom alias for new connection, or alias of existing connection to select. | -| `%typedb` | `-l` | List currently open connections. | -| `%typedb` | `-k ` | Close a connection by name. | -| `%typedb` | `-x ` | Close a connection by name and delete its database. | -| `%typeql` | `-r ` | Assign query result to the named variable instead of printing. | -| `%typeql` | `-f ` | Read in query from a TypeQL file at the specified path. | -| `%typeql` | `-i ` | Enable (`True`) or disable (`False`) rule inference for query. | -| `%typeql` | `-s ` | Force a particular session type for query, `schema` or `data`. | -| `%typeql` | `-t ` | Force a particular transaction type for query, `read` or `write`. | - -## Planned features - -- Add option to close all connections. -- Add more output formats. - -## Acknowledgements - -Many thanks to Catherine Devlin and all the contributors to -[ipython-sql](https://github.com/catherinedevlin/ipython-sql), which served as -the basis for this project. \ No newline at end of file +# Jupyter magic for TypeDB 3.x +Last updated for 3.0.4. + +### Getting started + Please + ```bash + cd src; + python3 -m jupyter notebook + ``` +See the [sample](src/Sample.ipynb) & [graph](src/graphs.ipynb) notebooks for more. diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 4a0383f..05ce53c 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,16 @@ # TypeDB Jupyter connector +## Version 0.5 +- Bump TypeDB Driver dependency to 2.28.4. + +## Version 0.4 +- A TypeQL output format has been added for `match` queries. The TypeQL returned contains all the necessary information +to reconstruct the original query in a new database. To do so: commit the same schema used for the initial database, +then place the returned TypeQL in an insert query and run it. When the original query is run on the new database, the +same results will be returned as with the initial database. This output format is not available for queries with `group` +or `aggregate` modifiers. +- Fixed bug in parsing of queries containing sub-pattern blocks (disjunctions and negations). + ## Version 0.3 - The `%tql` magic command has been replaced with two new ones: `%typedb` and `%typeql`. `%typedb` is used for opening diff --git a/pyproject.toml b/pyproject.toml index c036250..46eea2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "typedb-jupyter" -version = "0.3" +version = "0.4" description = "Jupyter connector for TypeDB" readme = "README.md" requires-python = ">=3.7" @@ -22,12 +22,13 @@ classifiers = [ "Topic :: Database", ] dependencies = [ - "typedb-client~=2.17", + "typedb-driver~=3.0.2", + "netgraph~=4.13.2", "ipython" ] [project.urls] "Repository" = "https://github.com/typedb-osi/typedb-jupyter" "Release notes" = "https://github.com/typedb-osi/typedb-jupyter/blob/master/RELEASE_NOTES.md" -"TypeDB" = "https://github.com/vaticle/typedb" -"Vaticle" = "https://vaticle.com/" +"TypeDB" = "https://github.com/typedb/typedb" +"Vaticle" = "https://typedb.com/" diff --git a/src/Sample.ipynb b/src/Sample.ipynb new file mode 100644 index 0000000..b94d0e2 --- /dev/null +++ b/src/Sample.ipynb @@ -0,0 +1,660 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "74a87d1f-52e4-458e-af4d-6db4f9bc3c27", + "metadata": {}, + "source": [ + "# TypeDB Jupyter\n", + "`typedb-jupyter` is a python library that introduces a few useful jupyter commands as well as python functions to enable users to work with TypeDB through jupyter notebooks, without having to pass through too much of the python driver.\n", + "\n", + "The `%typedb` 'line magic' allows administrative server like user management, database management and transactional commands.\n", + "The `%%typeql` 'cell magic' runs a query within the active transaction.\n", + "\n", + "To load the typedb-jupyter extension, we use:" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "adcdc08e-702a-4c23-acfb-d9f8b9612915", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext typedb_jupyter" + ] + }, + { + "cell_type": "markdown", + "id": "7457a903-f29f-47a0-be6b-de06aba502a2", + "metadata": {}, + "source": [ + "#### Open a connection" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "c78d12da-305a-4eca-bdc8-a19441fb521a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: connect [-h] [--tls-enabled]\n", + " {open,close,help} [{core,cluster}] [address] [username]\n", + " [password]\n", + "\n", + "Establishes the connection to TypeDB\n", + "\n", + "positional arguments:\n", + " {open,close,help}\n", + " {core,cluster}\n", + " address\n", + " username\n", + " password\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " --tls-enabled Use for encrypted servers\n", + "\n" + ] + } + ], + "source": [ + "%typedb connect help" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "484f0530-a414-4658-95ce-1ae6367a77cb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened connection to: 127.0.0.1:1729\n" + ] + } + ], + "source": [ + "%typedb connect open core 127.0.0.1:1729 admin password" + ] + }, + { + "cell_type": "markdown", + "id": "75c2970b-a4b0-47e5-94bf-0f26ee466487", + "metadata": {}, + "source": [ + "## Database Management\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "d7968c05-f1a8-4318-857b-fa4c349b95eb", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "usage: database [-h] {create,recreate,list,delete,schema,help} [name]\n", + "\n", + "Database management\n", + "\n", + "positional arguments:\n", + " {create,recreate,list,delete,schema,help}\n", + " name\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + "\n" + ] + } + ], + "source": [ + "%typedb database help" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "1ef0b8de-4e09-4588-bea7-f67fce0bfe95", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Created database typedb_jupyter_sample\n" + ] + } + ], + "source": [ + "%typedb database create typedb_jupyter_sample" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "2775578e-cbe6-498d-82c6-18d8d4c4f0c0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Databases: typedb_jupyter_graphs, typedb_jupyter_sample, tests, elgud\n" + ] + } + ], + "source": [ + "%typedb database list" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "6415f8cf-34e7-42b8-9294-0113c584fc33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Deleted database typedb_jupyter_sample\n" + ] + } + ], + "source": [ + "%typedb database delete typedb_jupyter_sample" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "85c80d7e-566b-4b92-9704-e27f68a58919", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Databases: typedb_jupyter_graphs, tests, elgud\n" + ] + } + ], + "source": [ + "%typedb database list" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "be17ff6a-8020-43a4-9611-2f5def7bab0d", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recreated database typedb_jupyter_sample\n" + ] + } + ], + "source": [ + "%typedb database recreate typedb_jupyter_sample" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "0cd600f3-6f6d-4b03-b3ec-fcf9b593e4b0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Databases: typedb_jupyter_graphs, typedb_jupyter_sample, tests, elgud\n" + ] + } + ], + "source": [ + "%typedb database list" + ] + }, + { + "cell_type": "markdown", + "id": "17144144-4053-450d-9a57-3f2bfacf4775", + "metadata": {}, + "source": [ + "## Transactions & queries\n", + "To query TypeDB, one needs to use transactions. \n", + "### Defining the schema\n", + "Open a `schema` transaction, define our schema, and commit." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "bfeae364-194b-4478-b02d-2b29c1ede228", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened schema transaction on database 'typedb_jupyter_sample' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_sample schema" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "d3a65846-4d15-4376-bfb1-c3c921355aab", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query completed successfully! (No results to show)\n" + ] + } + ], + "source": [ + "%%typeql \n", + "define\n", + " attribute name, value string;\n", + " entity person, owns name;\n" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "76949fbe-c0fc-4973-ad3b-b0a60c3499d4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction committed\n" + ] + } + ], + "source": [ + "%typedb transaction commit" + ] + }, + { + "cell_type": "markdown", + "id": "f883438e-6c35-4499-a3f0-4aa4dbda9dad", + "metadata": {}, + "source": [ + "### Writing data\n", + "Open a `write`, insert some data, and commit. \n", + "Notice that the insert query does return the data." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "2385b0db-a4b5-4b5e-b734-64ccc473780a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened write transaction on database 'typedb_jupyter_sample' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_sample write" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "d3a6084f-0bda-4985-a6a5-be1e93d138be", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 1 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
p
Entity(person: 0x1e00000000000000000000)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $p: Entity(person: 0x1e00000000000000000000) |]" + ] + }, + "execution_count": 15, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql\n", + "insert \n", + "$p isa person, has name \"James\";" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ea614f7f-c26f-4147-afb1-e1b0545744a3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction committed\n" + ] + } + ], + "source": [ + "%typedb transaction commit" + ] + }, + { + "cell_type": "markdown", + "id": "8beaad15-5ed0-467a-9a8e-edcf17eb3317", + "metadata": {}, + "source": [ + "#### Reading data\n", + "We can read data through `match` queries, with a `fetch` at the end if desired. The collected result is stored automatically in the `_typeql_result` python variable" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "381ab58e-fc12-43cb-a3dc-7a4aeba68da3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened read transaction on database 'typedb_jupyter_sample' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_sample read " + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "dbc8419c-ca70-43d2-94d2-48553f3c3a20", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 2 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
instanceinstance-type
Entity(person: 0x1e00000000000000000000)EntityType(person)
Attribute(name: \"James\")AttributeType(name)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $instance: Entity(person: 0x1e00000000000000000000) | $instance-type: EntityType(person) |,\n", + " | $instance: Attribute(name: \"James\") | $instance-type: AttributeType(name) |]" + ] + }, + "execution_count": 18, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql \n", + "match $instance isa! $instance-type;" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "13ee267b-041c-4847-9622-49b8b5d7bd27", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[| $instance: Entity(person: 0x1e00000000000000000000) | $instance-type: EntityType(person) |, | $instance: Attribute(name: \"James\") | $instance-type: AttributeType(name) |]\n" + ] + } + ], + "source": [ + "# As usual, the result can be accessed through the `_` variable.\n", + "print(_)" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "b835ea11-45fb-4b60-a171-b6245cf4d073", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query string was:\n", + "\t match $instance isa! $instance-type;\n", + "\n", + "First row of the result:\n", + "\t | $instance: Entity(person: 0x1e00000000000000000000) | $instance-type: EntityType(person) |\n" + ] + } + ], + "source": [ + "# Additionally, the result is stored in the `_typeql_result`, and the query string in `typeql_query_string`\n", + "print(\"Query string was:\\n\\t\", _typeql_query_string)\n", + "print(\"First row of the result:\\n\\t\", _typeql_result[0])" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "ef9f8c8c-6a88-4530-813d-d45844ef3293", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 1 documents.\n", + "{\n", + " \"attributes\": {\n", + " \"name\": \"James\"\n", + " }\n", + "}\n" + ] + }, + { + "data": { + "text/plain": [ + "[{'attributes': {'name': 'James'}}]" + ] + }, + "execution_count": 21, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql \n", + "match $owner isa! $owner_type; entity $owner_type;\n", + "fetch {\n", + " \"attributes\": { $owner.* }\n", + "};" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "9c48180e-84b5-4b0c-b2b6-3611640193d3", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction closed\n" + ] + } + ], + "source": [ + "%typedb transaction close" + ] + }, + { + "cell_type": "markdown", + "id": "4e34041a-d60c-4278-bb31-c82b4cd8a200", + "metadata": {}, + "source": [ + "## Miscellaneous\n", + "One can list all available commands:" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "c4b2cf02-baa2-4e71-86c3-824030318ee9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Available commands: connect, database, transaction, help\n", + "--------------------------------------------------------------------------------\n", + "Help for command 'connect':\n", + "usage: connect [-h] [--tls-enabled]\n", + " {open,close,help} [{core,cluster}] [address] [username]\n", + " [password]\n", + "\n", + "Establishes the connection to TypeDB\n", + "\n", + "positional arguments:\n", + " {open,close,help}\n", + " {core,cluster}\n", + " address\n", + " username\n", + " password\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + " --tls-enabled Use for encrypted servers\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Help for command 'database':\n", + "usage: database [-h] {create,recreate,list,delete,schema,help} [name]\n", + "\n", + "Database management\n", + "\n", + "positional arguments:\n", + " {create,recreate,list,delete,schema,help}\n", + " name\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Help for command 'transaction':\n", + "usage: transaction [-h]\n", + " {open,close,commit,rollback,help} [database]\n", + " [{schema,write,read}]\n", + "\n", + "Opens or closes a transaction to a database on the active connection\n", + "\n", + "positional arguments:\n", + " {open,close,commit,rollback,help}\n", + " database Only for 'open'\n", + " {schema,write,read} Only for 'open'\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + "\n", + "--------------------------------------------------------------------------------\n", + "Help for command 'help':\n", + "usage: help [-h]\n", + "\n", + "Shows this help description\n", + "\n", + "options:\n", + " -h, --help show this help message and exit\n", + "\n" + ] + } + ], + "source": [ + "%typedb help" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/__init__.py b/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/graphs.ipynb b/src/graphs.ipynb new file mode 100644 index 0000000..2754946 --- /dev/null +++ b/src/graphs.ipynb @@ -0,0 +1,807 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "4a203691-58f8-4c75-be61-25f381a0c73a", + "metadata": {}, + "source": [ + "# Visualisation\n", + "We use the [netgraph](https://github.com/paulbrodersen/netgraph) library along with [matplotlib](https://matplotlib.org) to visualise graphs.\n", + "First, we set up some data. If you are unfamiliar with that part, view the [Sample notebook](./Sample.ipynb) first" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "cf791e21-3eed-4b5d-a603-d993fe5c6b79", + "metadata": {}, + "outputs": [], + "source": [ + "%reload_ext typedb_jupyter" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "52cc65c9-5c72-4c96-b7f9-09cdb0fedd9f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened connection to: 127.0.0.1:1729\n" + ] + } + ], + "source": [ + "%typedb connect open core 127.0.0.1:1729 admin password" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "21a1762c-2dbc-42d8-a820-e80e1bfff9e5", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Recreated database typedb_jupyter_graphs\n" + ] + } + ], + "source": [ + "%typedb database recreate typedb_jupyter_graphs" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "9db34e01-ffef-417d-b844-e058becd12e4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened schema transaction on database 'typedb_jupyter_graphs' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_graphs schema" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "18e296bd-a459-403b-b45e-2138a2a724c9", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query completed successfully! (No results to show)\n" + ] + } + ], + "source": [ + "%%typeql\n", + "\n", + "define \n", + "attribute name, value string;\n", + "entity person, owns name @card(0..), plays friendship:friend;\n", + "relation friendship, relates friend @card(0..);" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "9e62200e-9a4a-4c66-a3f1-84071c9c7317", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction committed\n" + ] + } + ], + "source": [ + "%typedb transaction commit" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "ddafea5a-6601-498c-9495-3568a0d2d85f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened write transaction on database 'typedb_jupyter_graphs' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_graphs write" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "ef91ea81-f7e6-46f1-99e2-94713edde1c7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 1 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
f12f23p1p2p3
Relation(friendship: 0x1f00000000000000000000)Relation(friendship: 0x1f00000000000000000001)Entity(person: 0x1e00000000000000000000)Entity(person: 0x1e00000000000000000001)Entity(person: 0x1e00000000000000000002)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $f12: Relation(friendship: 0x1f00000000000000000000) | $f23: Relation(friendship: 0x1f00000000000000000001) | $p1: Entity(person: 0x1e00000000000000000000) | $p2: Entity(person: 0x1e00000000000000000001) | $p3: Entity(person: 0x1e00000000000000000002) |]" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql\n", + "\n", + "insert \n", + "$p1 isa person, has name \"John\";\n", + "$p2 isa person, has name \"James\";\n", + "$p3 isa person, has name \"James\", has name \"Jimmy\";\n", + "$f12 isa friendship, links (friend: $p1, friend: $p2);\n", + "$f23 isa friendship, links (friend: $p2, friend: $p3);" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "be72af7a-ff3f-446a-9952-e1f203fc1fc0", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction committed\n" + ] + } + ], + "source": [ + "%typedb transaction commit" + ] + }, + { + "cell_type": "markdown", + "id": "7c3ef991-5bca-45e9-9417-457a9f8b7b4a", + "metadata": {}, + "source": [ + "# Visualisation\n", + "\n", + "### Intialise matplotlib\n", + "Initialise matplotlib first. The `widget` mode allows interactive graphs inline." + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "4417485b-2d6f-45df-88db-a268a1c6d2a8", + "metadata": {}, + "outputs": [], + "source": [ + "%matplotlib widget\n", + "import matplotlib.pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "id": "e2b6cbff-d4dc-40a8-9328-1af6ccdeb19f", + "metadata": {}, + "source": [ + "### Read some data" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "1e03723d-b915-4438-a188-9020f9315a33", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened read transaction on database 'typedb_jupyter_graphs' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_graphs read" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "2a986872-2a81-4944-99fa-cc1fb3e135ee", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 4 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
np
Attribute(name: \"John\")Entity(person: 0x1e00000000000000000000)
Attribute(name: \"James\")Entity(person: 0x1e00000000000000000001)
Attribute(name: \"James\")Entity(person: 0x1e00000000000000000002)
Attribute(name: \"Jimmy\")Entity(person: 0x1e00000000000000000002)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $n: Attribute(name: \"John\") | $p: Entity(person: 0x1e00000000000000000000) |,\n", + " | $n: Attribute(name: \"James\") | $p: Entity(person: 0x1e00000000000000000001) |,\n", + " | $n: Attribute(name: \"James\") | $p: Entity(person: 0x1e00000000000000000002) |,\n", + " | $n: Attribute(name: \"Jimmy\") | $p: Entity(person: 0x1e00000000000000000002) |]" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql\n", + "match $p isa person, has name $n;" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "fa1e3c89-3901-4390-babc-2ec3ba3c0fe4", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction closed\n" + ] + } + ], + "source": [ + "%typedb transaction close" + ] + }, + { + "cell_type": "markdown", + "id": "030150b5-5364-49a8-a671-194d38cbf618", + "metadata": {}, + "source": [ + "### Create a graph from this data\n", + "Sadly, the basic TypeDB answers do not hold any information about the query structure. Till it does, we use a simple parser to parse out the structure from the query and reconstruct the graph." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c1db76f6-996c-49e5-a6ac-4c6d7ef67a88", + "metadata": {}, + "outputs": [], + "source": [ + "from typedb_jupyter.utils.parser import TypeQLVisitor\n", + "from typedb_jupyter.graph.query import QueryGraph\n", + "\n", + "parsed = TypeQLVisitor.parse_and_visit(_typeql_query_string)\n", + "query_graph = QueryGraph(parsed)" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "51cb2feb-1fa8-4b3c-9d27-94c65706dc2f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Entity(person: 0x1e00000000000000000000)--[has]-->Attribute(name: \"John\")\n", + "Entity(person: 0x1e00000000000000000001)--[has]-->Attribute(name: \"James\")\n", + "Entity(person: 0x1e00000000000000000002)--[has]-->Attribute(name: \"James\")\n", + "Entity(person: 0x1e00000000000000000002)--[has]-->Attribute(name: \"Jimmy\")\n" + ] + } + ], + "source": [ + "# Combine the data & the parsed query structure into the data-graph\n", + "from typedb_jupyter.graph.answer import AnswerGraph\n", + "\n", + "answer_graph = AnswerGraph.build(query_graph, _typeql_result)\n", + "print(\"\\n\".join(\",\".join(map(str, edges)) for edges in answer_graph.edges)) # We now have a list of (list of edges) per answer" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "ea10b692-7ff7-4d84-a683-4922c9a8d057", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8da846d6693c45f8b70ebe0b060fa092", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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9OGHH9pfz8vLKwoKCiqaNm2aOV+9erVp26pVq+z3fP311+Zadna2OX/mmWdMWzMyMuz3TJkypahv377l+vMD/siltgMoAADl0bVrV4fzVq1aSVJSkvl91apV8sorr8iePXskIyNDCgoKJCcnx/T66ZCq0p9t2rSxvz8gIMAMp3p7eztcsz1zx44dZji4Xbt2Zw3pNmvWzH6un1kW7bXLz8+X/v3726+5urpKnz59ZPfu3WV+P/1uStsSFhZmfte2NmnSpNTvD1QUARAAUC9ocCpJh15Pnz4tMTExMmLECLn33nvlpZdeMnME161bJ3feeafk5eXZA2Bp7y/rmUrnEDo7O5u5efqzpJKhsTq+n7ZD2dpSVvtLvg5UBAEQAFCvaUDTIKTz53QuoPr0008r/dwePXqYHkDtZbv88ssv6Bna4+jm5ibr16+X8PBwc017BHURyEMPPVTpNgIXikUgAIB6TRdVaKiaNWuWHDp0yCzmePfddyv9XB36HTdunNx2222yaNEiiY6Olp9++skMNX/99df2+zp06CCLFy8u9Rm6aEV7JqdMmWIWnvz2228yceJEMzStPZRAbSEAAgDqtW7dupkyMFOnTpXOnTvLhx9+aEJaVZg7d64JgA8//LApH6MFnrX3zjYvT2m5lvT0dPu59kbqKl4bXal7/fXXy6233io9e/aUAwcOyDfffGPKzgC1xUlXgtTapwMA0MBomRjtlXzrrbdquylAmegBBACgCqSmppr6fFpfcPDgwbXdHOCcWAQCAEAV0MLNOjysw8XXXXddbTcHOCeGgAEAACyGIWAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAsBgCIAAAgMUQAAEAACyGAAgAAGAxBEAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAsBgCIAAAgMUQAAEAACyGAAgAAGAxBEAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAsBgCIAAAgMUQAAEAACyGAAgAAGAxBEAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAsBgCIAAAgMUQAAEAACyGAAgAAGAxBEAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAsBgCIAAAgMUQAAEAACyGAAgAAGAxBEAAAACLIQACAABYDAEQAADAYgiAAAAAFkMABAAAEGv5//mjbQCvU9x4AAAAAElFTkSuQmCC", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# The AnswerGraph plot method will plot onto the matplotlib plot. \n", + "plt.figure() # For cleanliness, we'll tell matplotlib to create a new \"figure\" each time\n", + "plot_instance_1 = answer_graph.plot() # Limitations of netgraph require that you hold on to the returned value" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "id": "570974ff-86e3-4171-bc86-1522a6892aed", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "346c7546168e4a62a614fd1eb06a4399", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from typedb_jupyter.graph import visualise\n", + "plt.figure()\n", + "plot_instance_2 = visualise(_typeql_query_string, _typeql_result)" + ] + }, + { + "cell_type": "markdown", + "id": "8d4d479f-3c5e-4c42-9f4e-4024fd182abf", + "metadata": {}, + "source": [ + "## Some more examples" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "88b1e385-ab1c-464c-956d-b0a131789dc7", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened read transaction on database 'typedb_jupyter_graphs' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_graphs read" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "83e7c39b-a24d-4e35-b141-1a9474d50e51", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 6 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
ffriendn1n2p1p2
Relation(friendship: 0x1f00000000000000000000)RoleType(friendship:friend)Attribute(name: \"John\")Attribute(name: \"James\")Entity(person: 0x1e00000000000000000000)Entity(person: 0x1e00000000000000000001)
Relation(friendship: 0x1f00000000000000000000)RoleType(friendship:friend)Attribute(name: \"James\")Attribute(name: \"John\")Entity(person: 0x1e00000000000000000001)Entity(person: 0x1e00000000000000000000)
Relation(friendship: 0x1f00000000000000000001)RoleType(friendship:friend)Attribute(name: \"James\")Attribute(name: \"James\")Entity(person: 0x1e00000000000000000001)Entity(person: 0x1e00000000000000000002)
Relation(friendship: 0x1f00000000000000000001)RoleType(friendship:friend)Attribute(name: \"James\")Attribute(name: \"Jimmy\")Entity(person: 0x1e00000000000000000001)Entity(person: 0x1e00000000000000000002)
Relation(friendship: 0x1f00000000000000000001)RoleType(friendship:friend)Attribute(name: \"James\")Attribute(name: \"James\")Entity(person: 0x1e00000000000000000002)Entity(person: 0x1e00000000000000000001)
Relation(friendship: 0x1f00000000000000000001)RoleType(friendship:friend)Attribute(name: \"Jimmy\")Attribute(name: \"James\")Entity(person: 0x1e00000000000000000002)Entity(person: 0x1e00000000000000000001)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $f: Relation(friendship: 0x1f00000000000000000000) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"John\") | $n2: Attribute(name: \"James\") | $p1: Entity(person: 0x1e00000000000000000000) | $p2: Entity(person: 0x1e00000000000000000001) |,\n", + " | $f: Relation(friendship: 0x1f00000000000000000000) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"James\") | $n2: Attribute(name: \"John\") | $p1: Entity(person: 0x1e00000000000000000001) | $p2: Entity(person: 0x1e00000000000000000000) |,\n", + " | $f: Relation(friendship: 0x1f00000000000000000001) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"James\") | $n2: Attribute(name: \"James\") | $p1: Entity(person: 0x1e00000000000000000001) | $p2: Entity(person: 0x1e00000000000000000002) |,\n", + " | $f: Relation(friendship: 0x1f00000000000000000001) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"James\") | $n2: Attribute(name: \"Jimmy\") | $p1: Entity(person: 0x1e00000000000000000001) | $p2: Entity(person: 0x1e00000000000000000002) |,\n", + " | $f: Relation(friendship: 0x1f00000000000000000001) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"James\") | $n2: Attribute(name: \"James\") | $p1: Entity(person: 0x1e00000000000000000002) | $p2: Entity(person: 0x1e00000000000000000001) |,\n", + " | $f: Relation(friendship: 0x1f00000000000000000001) | $friend: RoleType(friendship:friend) | $n1: Attribute(name: \"Jimmy\") | $n2: Attribute(name: \"James\") | $p1: Entity(person: 0x1e00000000000000000002) | $p2: Entity(person: 0x1e00000000000000000001) |]" + ] + }, + "execution_count": 19, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql\n", + "match\n", + "$f isa friendship, links ($friend: $p1, $friend: $p2);\n", + "$p1 has name $n1;\n", + "$p2 has name $n2;" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "46d56f54-1068-4eae-9b58-4124a7aba5c1", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction closed\n" + ] + } + ], + "source": [ + "%typedb transaction close" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "c2d6529f-fee4-491b-be1b-6220193657d9", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/krishnangovindraj/code/side/typedb-jupyter/src/.venv/lib/python3.11/site-packages/netgraph/_parser.py:23: UserWarning: Multi-graphs are not properly supported. Duplicate edges are plotted as a single edge; edge weights (if any) are summed.\n", + " warnings.warn(msg)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "3f17c107a8d44cafbcf0f1abe91365ea", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAoAAAAHgCAYAAAA10dzkAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjAsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvlHJYcgAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfwVJREFUeJzt3Qd0lNXWBuANIZAeSEISOoGQ0HvvHQSUoiDKFbEgdi/Wy2+hiGJFBUWxIjYUEQSkV+ldeu8tpPcO+de7cYaZFFommfY+a2Ul8037AknmnXPO3qdETk5OjhARERGR0yhp7RMgIiIiouLFAEhERETkZBgAiYiIiJwMAyARERGRk2EAJCIiInIyDIBEREREToYBkIiIiMjJMAASERERORkGQCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETkZBkAiIiIiJ8MASERERORkGACJiIiInAwDIBEREZGTYQAkIiIicjIMgEREREROhgGQiIiIyMkwABIRERE5GQZAIiIiIifDAEhERETkZBgAiYiIiJwMAyARERGRk2EAJCIiInIyDIBEREREToYBkIiIiMjJMAASERERORkGQCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETkZBkAiIiIiJ8MASERERORkGACJiIiInAwDIBEREZGTYQAkIiIicjIMgEREREROhgGQiIiIyMkwABIRERE5GQZAIiIiIifDAEhERETkZBgAiexMTk6OPPbYY+Ln5yclSpSQf/75J9/b4bp58+YV+flUr15dPv74Y4vc9tSpU9f9nvIzY8YMvQ8+/vvf/4olrVmzxvjYAwYMsOhjExFZEwMgkZ1ZsmSJhp6FCxfKxYsXpX79+vneDtfdcccdYk+qVKly3e+pID4+Pnq/N9980ywov/HGG1KhQgVxd3eX7t27y9GjR83uFxsbK8OGDdP7ly1bVh555BFJTk42Xt+2bVt93CFDhljguyMish0MgER25vjx4xpqEE6Cg4OlVKlSZtdnZmbqZ1xXpkwZsScuLi75fk83ghE63M/b29t47L333pMpU6bIF198IVu2bBFPT0/p1auXpKenG2+D8Ld//35Zvny5Buq///5bR1cN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9Om6DtPb21vX+WFEEI2isWUcmkI/9thjMmDAAG0Xw51BiIhuHzuqEpHN8PDw0PAHmP59//33da/g2NhYefvtt2X+/PnyxBNP6PVsCG2fUCiFIO/n56e9HP/55598b4fr0Bi8qFWvXl2XHVjitqdOnbru95SfGTNm6H3w8d///lcsyXA++DDdbYcI+BeUiGzOokWLtAhkwoQJMnXqVB3969evn3z77be6RdzevXv1RQ1FIWRfEOgRehYuXCgXL17UKf/84Lo77rhD7EmVKlWu+z0VBIVOuB+q3U2D8htvvCEVKlTQ5Q/du3eXo0ePmt3vrbfekrZt2+obJ+yUU9D5vPDCC4X4rshRMQASkc3ZsmWL9OzZU+677z4ZOHCguLm5yTfffKNTvygOWbx4sd6Ou4HYn+PHj2uoQXAJDg7WBuCm0OgbcJ2hKtxeYElCft/TjeDnGPczjH4D3gBhLewXX3yhvw9omN6rVy9JT083+7caPHiwcVS8oPPBWlqi3BgAicjmnDt3ToKCgoyXhw8fLr/++qt+HR0drQUiwABoX0aMGCHPPPOMnDlzRv/vMKWKLf+efvppnf7E7i4IOflNAZ89e1abgGOkC9PH/fv31ylO08fG+lAsHUDA9Pf3l6eeekoLiAzQQ/LOO+/UEbWQkBD56aefzM4Po27jxo2TqlWravhE9fmzzz5rdpvU1FR5+OGHNazhdlinWtAU8Jo1a/Qy9rlu2LChvpHBz+6+ffuu+++E88BU82uvvabfJ+47c+ZMuXDhgtm/yfjx42X06NHSoEGD2/jfIGfHAEhENmfo0KGyYMECSfh3W70HH3xQVq1apS/8eBENDw+39inSbfjkk090Wh8juZia3LZtmx7//vvvddeXDRs26IhXbghxCIYIXagUx+0wqoU9pA0jhrB69WodYcRnPCammvFhGhIRJHH977//LtOmTdNQaDBnzhzdcxrFSJhuRdjKHa4+/PBDad68uezatUuefPJJHX07fPjwdb/vl156Se+H77d8+fIaQk2DaW4nT56UiIgInfY1wO43rVq1kk2bNt3w35noZrAKmIhsDvYErlatmq6JwigORmswGoIXaxSGYDqM7A9CDEKcYWrSoFatWjrlWRCM/mK959dff20c9f3uu+90NBCjbFguAOXKlZNPP/1UH7927drSt29fWblypYwcOVKOHDmi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9fJr/31CyvtbvriEiBwHAyARkRXd27+PPHr/YP26XERVqbGjvbgl+dz0/d0Tykro9o7iG1VRLz8/6iG5o2vHIjtfInIMJXK4UzgRkVXhz/CMX+fKlK9nSvbly7qLR5L/JYmteFpSysZIjov5jiEls13EMz5A/M5XE++4q+v80O4F074s/CCim8EASERkIw4fOymvvfuxHDp2wngMYTDDM0myS2fo5VIZbto82rDTBzSqV1vefPk5Cala2SrnTUT2hwGQiMiGYO/eDVt3yqw/F8n6rTt0dDA/LiVLSud2rXQKuXXTRlKihOW3lCMix8UASERko1LT0nQ08ODRE5KYlKwhr6yPt9SuVVPCa1YXdzc3a58iEdkpBkAiIiIiJ8MqYCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhEVgxEjRsiAAQOsfRpERIoBkIiIiMjJMAASkd3q3LmzPPvss/Lyyy+Ln5+fBAcHy7hx44zXT548WRo0aCCenp5SpUoVefLJJyU5Odl4/YwZM6Rs2bKycOFCCQ8PFw8PD7nnnnskNTVVvv/+e6levbqUK1dOnwN79BpkZGTIiy++KJUqVdLHbtWqlaxZs+aWzn3JkiXSvn17fX5/f3/p16+fHD9+3Hj9qVOndOu33377TTp06CDu7u7SokULOXLkiGzbtk2aN28uXl5ecscdd0hUVJTZY3/99ddSp04dcXNzk9q1a8u0adOM12VmZsrTTz8tFSpU0OurVasmkyZNuuV/eyKybwyARGTXENQQwrZs2SLvvfeeTJgwQZYvX67XlSxZUqZMmSL79+/X261atUrDoimEPdxm1qxZGsoQ5AYOHCiLFi3Sjx9++EGmT58uv//+u/E+CFCbNm3S++zZs0cGDx4svXv3lqNHjxpvg/CGgFmQlJQUef7552X79u2ycuVKPVc875UrV8xuN3bsWHnttddk586dUqpUKbn//vv1e/jkk09k3bp1cuzYMXnjjTeMt//pp5/08ltvvSUHDx6Ut99+W15//XX9/gHf6/z58zVYHj58WG+PoEtETgZ7ARMR2aNOnTrltG/f3uxYixYtcl555ZV8bz979uwcf39/4+XvvvsOe6HnHDt2zHhs1KhROR4eHjlJSUnGY7169dLjcPr06RwXF5ec8+fPmz12t27dcsaMGWO8HB4envPHH38YLz/44IM5/fv3L/B7iYqK0nPZu3evXj558qRe/vrrr423+eWXX/TYypUrjccmTZqkz2VQs2bNnJ9//tnssd98882cNm3a6NfPPPNMTteuXXOuXLlS4LkQkeMrZe0ASkRUGA0bNjS7jKnNyMhI/XrFihU6vXno0CFJTEyU7OxsSU9P11E/TPcCPtesWdN4/6CgIB0Rw/Sq6THDY+7du1eng8PCwsyeF9PCmMo1wHNeD0YLMVKHkcvo6GjjyN+ZM2ekfv36+X5/OA/AtHZ+54ZRRUwjP/LIIzJy5EjjbfB9+/r6GotRevTooVPeGLXE1HPPnj2ve65E5HgYAInIrrm6uppdxtQrwhTW0CHcPPHEEzodijWC69ev13CEdXCGAJjf/Qt6TMAaQhcXF9mxY4d+NmUaGm/kzjvv1PV3X331lVSsWFEfH8EP51bQ94fzyO+Y6bkBHhPrEk0ZzrVp06Zy8uRJWbx4sQbkIUOGSPfu3c2muInI8TEAEpFDQkBDMPrwww91fR1g3VthNWnSREcAMeqG4ozbERMTo+vvENQMj4FwWlgYDUSYPHHihAwbNqzA2/n4+Mi9996rHyh6wUhgbGyshmQicg4MgETkkEJDQyUrK0umTp2qo20bNmyQL774otCPi6lfhKvhw4druEQgRBUuCjkwXdu3b1+9HapvMf2Mwo7cUFmM6eIvv/xSp6wx7fu///1PLGH8+PFatYwpXwQ7TE2j0CQuLk6LTlAZjefEeSMYz549W6unUY1MRM6DVcBE5JAaNWqkYefdd9/VqVVUu1qq3cl3332nAfCFF17QtXRo8IzWLFWrVjXeBiN8CQkJxssYjUQVLyB4oYIYo5Q4t9GjR8v7779vkXN79NFHtQ0MzhFrBTt16qTVyCEhIXq9t7e3VkujjQzaymCqHNXOhlFSInIOJVAJYu2TICJydBiNw6jkp59+au1TISLiCCARUVHC1CsaTaO/IIotiIhsAdcAEhEVoYcfflinhzFd3L9/f2ufDhGR4hQwERERkZPhFDARERGRk2EAJCIiInIyDIBEREREToYBkIiIiMjJMAASERERORkGQCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETkZBkAiIiIiJ8MASERERORkGACJiIiInAwDIBEREZGTYQAkIiIicjIMgEREREROhgGQiIiIyMkwABIRERE5GQZAIiIiIifDAEhERETkZBgAiYiIiJwMAyARERGRk2EAJCIiInIyDIBEREREToYBkIiIiMjJMAASERERORkGQCIiIiInwwBIRERE5GQYAImIiIicDAMgERERkZNhACQiIiJyMgyARERERE6GAZCIiIjIyTAAEhERETkZBkAiIiIiJ8MASERERORkGACJiIiInAwDIBEREZGTYQAkIiIicjIMgEREREROhgGQiIiIyMkwABIRERE5GQZAIiIiInEu/w/hDfHLy296+gAAAABJRU5ErkJggg==", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# As before, but we will use the convenience method this time\n", + "plt.figure()\n", + "plot_instance_3 = visualise(_typeql_query_string, _typeql_result)" + ] + }, + { + "cell_type": "markdown", + "id": "574ad2db-b5e9-46c1-8428-3ee9db97f164", + "metadata": {}, + "source": [ + "### Custom visualisation\n", + "The IGraphVisualisationBuilder provides an interface for easy building. But first we get an easy query" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "id": "896949b2-866e-4974-8a37-a91f46566be6", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Opened read transaction on database 'typedb_jupyter_graphs' \n" + ] + } + ], + "source": [ + "%typedb transaction open typedb_jupyter_graphs read" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "id": "b60dd08f-f717-418b-bac9-1e091a2a6c14", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Query returned 4 rows.\n" + ] + }, + { + "data": { + "text/html": [ + "
np
Attribute(name: \"John\")Entity(person: 0x1e00000000000000000000)
Attribute(name: \"James\")Entity(person: 0x1e00000000000000000001)
Attribute(name: \"James\")Entity(person: 0x1e00000000000000000002)
Attribute(name: \"Jimmy\")Entity(person: 0x1e00000000000000000002)
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/plain": [ + "[| $n: Attribute(name: \"John\") | $p: Entity(person: 0x1e00000000000000000000) |,\n", + " | $n: Attribute(name: \"James\") | $p: Entity(person: 0x1e00000000000000000001) |,\n", + " | $n: Attribute(name: \"James\") | $p: Entity(person: 0x1e00000000000000000002) |,\n", + " | $n: Attribute(name: \"Jimmy\") | $p: Entity(person: 0x1e00000000000000000002) |]" + ] + }, + "execution_count": 23, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "%%typeql\n", + "match $p has name $n;\n" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "63adcc82-e76f-4fca-9e6d-8a0fc6f66059", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Transaction closed\n" + ] + } + ], + "source": [ + "%typedb transaction close" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "de10958b-0a36-4000-85e2-f53e58ff7fff", + "metadata": {}, + "outputs": [], + "source": [ + "# You can also customise this greatly through the \n", + "from typedb_jupyter.graph.answer import IGraphVisualisationBuilder\n", + "from typedb_jupyter.graph.answer import EntityVertex, RelationVertex, AttributeVertex, HasEdge, LinksEdge\n", + "from typing import Any\n", + "\n", + "\n", + "class MyVisualisationBuilder(IGraphVisualisationBuilder):\n", + " \"\"\"\n", + " This class will colour edges belonging to the same query\n", + " \"\"\"\n", + " def __init__(self):\n", + " self.edges = []\n", + " self.edge_labels = dict()\n", + " self.edge_colours = dict()\n", + " self.current_colour = 0x000000000 # RGBA colour\n", + " self.node_labels = dict()\n", + "\n", + " def notify_start_next_answer(self, index: int):\n", + " # Change the colour for every new answer\n", + " self.current_colour = (self.current_colour + 0x3377bb00) % 0x100000000\n", + " \n", + " def add_entity_vertex(self, answer_index: int, vertex: EntityVertex):\n", + " self.node_labels[vertex] = \"ENT[%s:%s]\"%(vertex.type(), vertex.iid()[-4:])\n", + "\n", + " def add_relation_vertex(self, answer_index: int, vertex: RelationVertex):\n", + " self.node_labels[vertex] = \"REL[%s:%s]\"%(vertex.type(), vertex.iid()[-4:])\n", + "\n", + " def add_attribute_vertex(self, answer_index: int, vertex: AttributeVertex):\n", + " self.node_labels[vertex] = \"ATT[%s:%s]\"%(vertex.type(), vertex.iid())\n", + "\n", + " def add_has_edge(self, answer_index: int, edge: HasEdge):\n", + " pair = (edge.lhs,edge.rhs)\n", + " self.edges.append(pair)\n", + " self.edge_labels[pair] = \"has\"\n", + " self.edge_colours[pair] = \"#%0.8x\"%self.current_colour\n", + "\n", + " def add_links_edge(self, answer_index: int, edge: LinksEdge):\n", + " pair = (edge.lhs,edge.rhs)\n", + " self.edges.append(pair)\n", + " self.edge_labels[pair] = edge.role()\n", + " self.edge_colours[pair] = \"#%0.8x\"%self.current_colour\n", + "\n", + "\n", + " def plot(self) -> Any:\n", + " # https://netgraph.readthedocs.io/en/latest/index.html\n", + " from netgraph import BaseGraph # We use InteractiveGraph to allow dragging\n", + " return BaseGraph(\n", + " self.edges,\n", + " node_labels=self.node_labels,\n", + " edge_color=self.edge_colours,\n", + " node_layout='bipartite', # Try others: https://netgraph.readthedocs.io/en/latest/graph_classes.html#netgraph.InteractiveGraph\n", + " node_label_offset=(0,-0.05)\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "id": "2684cd61-ae9b-4fea-be4b-394e18c81bc2", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/Users/krishnangovindraj/code/side/typedb-jupyter/src/.venv/lib/python3.11/site-packages/netgraph/_node_layout.py:1214: UserWarning: The graph consistst of multiple components, and hence the partitioning into two subsets/layers is ambiguous!\n", + "Use the `subsets` argument to explicitly specify the desired partitioning.\n", + " warnings.warn(msg)\n", + "/Users/krishnangovindraj/code/side/typedb-jupyter/src/.venv/lib/python3.11/site-packages/netgraph/_utils.py:360: RuntimeWarning: invalid value encountered in divide\n", + " v = v / np.linalg.norm(v, axis=-1)[:, None] # unit vector\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "25bbb7a72cc04105b5f0557ed3ad7d7d", + "version_major": 2, + "version_minor": 0 + }, + "image/png": "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++yWW26xChUquK+5u+iii2zTpk3B+7/77jvr1auXq70lSpSwE044wZ599tmwffTs2dPV5dNPPz0XXy0ywlfB5WOvvPKK3Xbbbe7n+vXr3Yfr8ccfD95frVo1e+2114KFUuvsFSv2vwJ14YUXukD06KOPBm+rVKmS+6nCULVq+rPS8iMF5Vh+PXrfy5cvH+3DAGKSaphqWSjVKI9Cyj333GOffPJJhidY3neOL1y40IWVVatWWenSpd1tCihqeRcFGg03UfDLrRY/dbzpeIoWzfs/watXr3aNBjfeeKNNmDDBhd1rr73W/b3o3Lmz22bixIl255132osvvujCn1rmdJ/es8qVK7tt7rjjDps+fbq9++67VqZMGbv11lvd35QFC/6eyPfNN9+4bf/973+7EKj3/frrr3e1WNt677sucUxqiQ51ASP/2b17dyAxMTGwcuXKQM+ePQNDhw5Ns43+eadMmZLhPtq1axfo379/mtuvvPLKQPfu3cNuW716tdvfpEmTAu3btw+UKFEi0LRp08DChQuD22zdujVw2WWXBapXr+7ub9KkSeDNN99M85y33nqre96yZcsGKleuHHjppZcCSUlJgauuusq9prp16wZmzJgR9rhly5YFzjnnnEBCQoJ7TJ8+fQJbtmw54nsU+vq941+6dKm7Pn/+fHd91qxZgebNmweKFy8eOPPMMwObNm1yz92wYcNAqVKlAr169QokJydn+/gPHz7sro8YMSLs2HQcev6ff/45eJt3TNu3bz/i6wL8JL16FKp27dqBf/zjH4G4uLjA9OnTg7frM6rPa2oZfc68GjFn6oTA0lmvBb6eNsZdzjurVaDdac0C1/fqGihXplQgMSEhcP111wVSUlKCjz106FBg2LBhgTp16rhaotr47rvvpnlO1YUWLVoEihUr5m779ttvXT1V3VC90X2LFy8OPu69994LNGrUyL02vc6nnnoqzWtX7b/66qvdPmrVqhX417/+dcT3c+DAgYHGjRuH3aa/IZ07dw5eP/XUUwO33HJL2OtTXR8+fLi7vmPHDvcaQl/jihUr3GtctGhRhs998803uzqb2b9FyF10AedT77zzjjVs2NAaNGhgffr0sVdffdWdVea2+++/3+666y43PqZ+/fquif/gwYPBLoGTTz7ZnRX+8MMP7myvb9++aboONL6wYsWK7na1YN500012ySWXWOvWrW3JkiXWqVMn97g9e/a47Xfs2GFnnXWWnXTSSfb111/brFmzXFfDpZdeGtynujCyMzvukUceseeee86dna5du9btU2e7b775pnsdc+bMsdGjR2f7+HVM11xzTZrWC11v27at634BkDMau6cWrXvvvdetm5dVbozf6mXu9y1rlqdp9Vv8/SrbtOugffzJp/b2xIk2ZepUGzx4cPD+4cOH2/jx412L2fLly13rmOpy6hbJQYMGuV6aFStWWNOmTe3yyy+3mjVruhZKtZjpfq+XRtdVjy677DJbtmyZq1UPPvhgmu7ap59+2k455RQ3vvnmm2929UgtdR51g4d2cWvoUIcOHcL2odY93S779+93zx26jdZk1XVvG92vHqXQbfT36Jhjjgluk56dO3fS0xFLcjlgIpe0bt06MGrUKPf7gQMHAhUrVnRnlLndAjh27NjgbcuXL3e36cwvI127dg0MGDAg7DnPOOOM4PWDBw+6Vr2+ffsGb9uwYUPYmeSQIUMCnTp1Ctvv2rVr3TarVq1y1ydPnhxo0KBBllsA582bF9xeZ7e67ddffw3edsMNN4SdGWfn+NetWxcoUqRI4Msvv3TX9+/f7/69xo0bF3a8tAACgXTrkT4/+pyFXrxeD7WCjRw5MrB582bXijZ+/PhMtwAe3J8SWP/T167Fb9rLj7nbJ4y6L9j69+3scYFLL+wWKF++fFhPwJgxY1yLm1rG9u3bFyhZsmRYb4j069fP9SCEPufUqVPDttHxpq4Dnt69ewc6duwYdtvdd9/tWgQ9eu3qDfGox0G9Ejo+j2rToEGDgtfr1avnWitDqeVUx7dnzx5Xr/R76tej51bLoEyYMMG1SqbWsmVL18KYngULFgSKFi0amD17dpr7aAGMDsYA5kM6u1Prk2boisaRaDCtxgJ6g55zi85aPRozIps3b3ZnfxrTMmzYMNc6uW7dOncmmZKSYiVLlsxwHxoPojE3J554YvC2KlWqBPfrDSaeP3++G2ic2q+//upaInv06OEuOXk9el4d63HHHRd2W+oWzKwef/Xq1d2YG7XSnnrqqfbBBx+490WthgCO7swzz7QxY8aE3Za6JUnjaNU78dBDD7l6eDQbf/3W1mxdc9QxfiXenO9mBYfWMU1aSEpKcr0G+qnW/o4dO4btQ/VPvRah1FIXSuPsNP7ujTfecK1pqgl169Z196mVsHv37mHbt2nTxvVQqNaq9qSuR+px0Fhnr/aIWiajTT1Cei0PP/yw6yFBbCAA5kMKeup2VbDwqMFLg6LVnakBubkldBKJ1+XqdbmMGDHCTURRgVIgSkhIcDPxVAgz2oe3nyPtVwW2W7du9sQTT6Q5Hi+ERur1pHdsqbuUsnr8oiKvbuGRI0e67l/9gUodjAGkT7UkM8MlFKheeOEFd8moq3fbn393j278aYmVSkz7GaxyXFNrctaFmZ7cofokGjJSo0aNDCeqeK8jlLp1e/fu7R47c+ZMF5DefvvtLJ3MZqZmhVJADJ2tK7quCTGakKFgqUt623gT6fRTdV3Dc0KXrArdxvPjjz/a2Wef7YYEPfDAA5l+Xch9jAHMZxT8dEancR8ah+dd1EqmQPjWW29F7dg0+0tneRr7ojNmtaT99FP6q9tnhZYr0LgaLUmgPwKhl9QFNVade+657ljViqExjBoXCCCy1EugcXJDhw613bt3B28PXc5l6x//Gx8X1uJX7+/Wugq1GqQJf6qve/fuDV7XkjN6Ls1ubdSokQt6f/zxR5r6pPuPRj0YGjOo8caaReuNF9ayKd6MWo+ua3uv9S871HrpLXPjmTt3bnApFs3I1Vju0G0UKHXd20b3K3iGbqOeKb0HoUu6qG6r9fbKK690/yaILQTAfObDDz90yxX069fPLeESetHSBmodjJZ69eq5QqIJFeq+uOGGG9KcRWaH1pr666+/3IQTDZZWt+/s2bPdAqLe0g7qDlc3dKxSwdZAbA1S1/vEuldA5mnIxMaNG8MuW7duTXdbtTSpF0QTudQzktl1/CrVbpTh86u1SzVXrVkzZsxwLXVaykSTI0qVKuW6nhXiNEFM9UmTwTR5zFtQPz0KlNqH1uTTIvAKd6pvCn4yYMAAF7CGDBniTqS1L/Xw6Lmy4oorrnB1x6PJMr/99psNHDjQVq5c6VpLNWxHxx/akvryyy+751Qt18SS5OTk4KLNen/1fmg7Dc/RpBDdp7rWqlWrYLevwp+6fLWd9++2ZcuWLB0/cg9dwPmMAp7GiqTXzasA+OSTT9r3338fNi4kr6h5X4VFM8rUvalCrG8R0cyvnFDLpoqj1vlSMdEfg9q1a7u1wVSARc8ROvPN6wKJxjpbGVHB1BhJr4gCyBy1mqce7qEVEBRgUlPL1OCHH7Y+V1xhyds3ueCXnuoNW1rdE1tlqqtXXZg6cdPMfdUfnYyq+9ajkKYxiJoNrBqoblH1XNx3331HPCnctm2bC2g6UdbKAmoB9GYX6/EKZhrTqP3r9WvN1tAZvZmhVjmvTnozptXlrMCnITuahTx27NjgGoCiISoKanpuhTZ9e5L+DbzxzaLhLNqv/u7oPdHjQ7ve33vvPbcPrQOoi0e12/vCAUQXXwWHNFRgNLbD+xaN/EhFSwVTZ9SpB15Hy2effeb+kGjgeGgh9aglQGfMfBUckD05+cq2jL4usiDUw1inyYt6z/kquLxFFzAy7GrWGBf9zE90PqNC/thjj7mQFQtf/aaz4z///NO1GGiWX3rhr3Hjxvn6a/eAaIrkV7ZpPU9dkPv0TST6O6OTY+Q9WgCRhpYQ2LVrl/tdrWj5ZaKF6ExdAUvjaHQ2mdvL4mSGFm5V96/OcKdNm5ZmpqBoDJAWVhVNngntsgEQ+Ra/9CbYeV2TmtThTeCgBTD3aKKON05cvR7qBkfeIQACAHwb/AC/ip0R8gAAHAHBD4gcAiAAIKYR/IDIIwACAGISwQ/IPQRAAEBMIfgBuY8ACACICQQ/IO8QAAEAUUXwA/IeARAAEBUEPyB6CIAAgDxF8AOijwAIAMgTBD8gdhAAAQC5iuAHxB4CIAAgVxD8gNhFAAQARBTBD4h9BEAAQEQQ/ID8gwAIAMgRgh+Q/xAAAQDZQvAD8i8CIAAgSwh+QP5HAAQAZArBDyg4CIAAgCMi+AEFDwEQAJAugh9QcBEAAQBhCH5AwUcABAA4BD/APwiAAOBzBD/AfwiAAOBTBD/AvwiAAOAzBD8ABEAA8AmCHwAPARAACjiCH4DUCIAAUEAR/ABkhAAIAAUMwQ/A0RAAAaCAIPgByCwCIADkcwQ/AFlFAASAfIrgByC7CIAAkM8Q/ADkFAEQAPIJgh+ASCEAAkCMI/gBiDQCIADEKIIfgNxCAASAGEPwA5DbCIAAECMIfgDyCgEQAKKM4AcgrxEAASBKCH4AooUACAB5jOAHINoIgACQRwh+AGIFARAAchnBD0CsIQACQC4h+AGIVQRAAIgwgh+AWEcABIAIIfgByC8IgACQQwQ/APkNARAAsongByC/IgACQBYR/ADkdwRAAMgkgh+AgoIACABHQfADUNAQAAEgAwQ/AAUVARAAUiH4ASjoCIAA8F8EPwB+QQAE4HsEPwB+QwAE4FsEPwB+RQAE4DsEPwB+RwAE4BsEPwD4GwEQQIFH8AOAcARAAAUWwQ8A0kcABFDgEPwA4MgIgAAKDIIfAGQOARBAvkfwA4CsIQACyLcIfgCQPQRAAPkOwQ8AcoYACCDfIPgBQGQQAAHEPIIfAEQWARBAzCL4AUDuIAACiDkEPwDIXQRAADGD4AcAeYMACCDqCH4AkLcIgACihuAHANFBAASQ5wh+ABBdBEAAeYbgBwCxgQAIINcR/AAgthAAAeQagh8AxCYCIICII/gBQGwjAAKIGIIfAOQPBEAAOUbwA4D8hQAIINsIfgCQPxEAAWQZwQ8A8jcCIIBMI/gBQMFAAARwVAQ/AChYCIAAMkTwA4CCiQAIIA2CHwAUbARAAEEEPwDwBwIgAIIfAPgMARDwMYIfAPgTARDwIYIfAPgbARDwEYIfAEAIgIAPEPwAAKEIgEABRvADAKSHAAgUQAQ/AMCREACBAoTgBwDIDAIgUAAQ/AAAWUEABPIxgh8AIDsIgEA+RPADAOQEARDIRwh+AIBIIAAC+QDBDwAQSQRAIIYR/AAAuYEACMQggh8AIDcRAIEYQvADAOQFAiAQAwh+AIC8RAAEoojgBwCIBgIgEAUEPwBANBEAgTxE8AMAxAICIJAHCH4AgFhCAARyEcEPABCLCIBALiD4AQBiGQEQiCCCHwAgPyAAAhFA8AMA5CcEQCAHCH4AgPyIAAhkA8EPAJCfEQCBLCD4AQAKAgIgkAkEPwBAQUIABI6A4AcAKIgIgEA6CH4AgIKMAAiEIPgBAPyAAAgQ/AAAPkMAhK8R/AAAfkQAhC8R/AAAfkYAhK8Q/AAAIAAihh08dMh27U6ypORki4+LtzKlE614fHy29kXwAxDr9qWk2M5dSZayP8USExKsdKlEK1qkSLQPCwUUARAx55fVv9vEaTPtgzn/seQ9e8Pua33KSdaz+7nW9vSWmSqMBD8AsX6i+8mir+yd92fawq+Xht2XULKEdet0lvU8v4sdf2ztqB0jCqZCgUAgEO2DAOTb5Svt2Zdft6+/+8Fdr1ShvJ14Qn1LTChpKSn77c8NG235ql/cfVUrV7QrL+lhl1/UzQoVKpRmXwQ/ALFMf3r/PWmavf7OVNu0Zau7rXGD461mtaoWHx9nScl7bNmKn2zLtr/cfac0a2K3X3+lNWvUMMpHjoKCAIiYMOeTBXbv0Kdt/4EDdtpJTV0rX/s2p1mxouGN1Kt+WW0Tp82wD+d+bHv37bPzO59ljwy41YoV+zvEEfwAxLoDBw7YI089Z9Pm/MdKFC9u53Vsbz3PP9caHH9s+HYHD9r8BV/axPdn2FdLv7e4YsVs+P0DrFO7NlE7dhQcBEBE3ccLv7L+Dw61+Lg4e+rhe6xtq1OO+pitf223W+591H786Rfr1vFMe/SuW2zL7z8Q/ADENP3JvW/4M+4ktlH94+354Q9ZxfLljvq4T7/42u4a/ISl7N9v/3zsfmt3+ql5crwouAiAiKrVf/xpPW+43RXFl58eas0bZ757Y8/evXbNHfe5buHrLu5kbY5JG+wIfgBiyatvTbKRL42zJg3r2SvPDLWSJUpkaZjMtXfeb4ULF7KJ/xplxx5TM1ePFQVb4WgfAPxNY2D27kuxB++4JUvhT1Q4nxv2kJVKTLBJ8760w4WLhQW/mo1OtyZn9bYqdZsR/gBE3f79B+z1d6a4mjV66INZCn+iGvnQnbe4mjlh0rRcO074AwEQUaNBzh/MmW9VKlW0czu0y9Y+1HVywTkd7K8dO+3nHUUJfgBi1txPF7ha1aNLx0x1+6ZHtbJyxQr2wdyPXQ0FsosAiKhR+NNEjovP65yjta4uPb+L+znri+UEPwAxS5M5QmtWdqhWqmZqCMyHc+dH8OjgNwRARPVsWC7q2ilH+6lTq4adelJTW/rDCtu+KylCRwcAkaOJa6pRqlW1a1bP0b4UAGXupwsjdHTwIxaCRtRs37HTrfGn9f5ySgVVyyRM2TTbqpaoEJHjA4BI2bhxa/CENadUM7VItLqTgeyiBRBRszclJdtf7Zaa1tKSA/sORGR/ABBJB1IOup+RrHkaQgNkFwEQUZNYsqQl74nMIGZ9X7DEJ8RFZH8AEEnxJf+uTZGseaUSEiKyL/gTXcCImiqVKtiqX1fbr2v+sLp1jsn2frSGoNYCLFy4sF1crauVKZEY0eMEgJzaWT3J/ln4TVerVLPS+wrLzFLN3Jey3ypXzPnwGfgXLYCIGn3JubwzbWaO9vPdj6tckDyzzWlWphThD0DsUW1q3/pUW/nLb/b9ilU52tfE/9bM8zufHaGjgx8RABE1Z5/Ryq2Fpe/D1JIG2fX21Onup74/GABilVejvJqVHaqV02Z/5CaCnHVGqwgeHfyGAIioKVasmFvOQIuZvvb25GztQ2fTcz753M2sO+2kphE/RgCIlFYtmrkVC2Z//LmrXdn9KrnkPXtd7SxWlFFcyD4CIKLqsgu6WtXKFe3F8W/blJnzsvTYtes32i33DrYDBw7aP/r1dWMAASBWqUb1v/YKV7NUu1TDskI18l9vTHQ1kx4P5BR/MRFVFcqVtRcef8TNZnvoyWddcUvZv/+Ij9EA6sXfLrM+t95tm7f+ZXfecLV1bNcmz44ZALJLteqO669ytUs1TLVMNe1IVBN1kqwaqVqpmqnaCeREocDR/ucBeUCTOG4eNNg2b91m5cqUth7ndrRLup1jNatVDW6jNa9mfvSpvf3+DFvx869uFt09t15nl1/YLarHDgBZNWHSNHvi+bEu/DWqf7z1PL+LdTm7bXBNU/lzw0Z794NZNmXGXNu+c5f7DuAxTzxi9Y+rE9VjR8FAAETMUPgb/+5U182xa3eSC3gqeKUSStq+/ftt67a/3NIH6kZp26qlXXFJd2vZ/MRoHzYAZIta/1TzPlm02AXB4vFxVrFCeSseF2e7k/e4mqjbS5dKtB5dOtiVl/aIyDcnAUIARMzZl5Jis+Z/5oLghk2bLSlpj8XHx1nZ0qXcUi8Xn3eOVa9aOdqHCQARsX7jZnvvw1k2f8GXtmPXbktJ2W+JiSWtWpXKLvidc+b/RewbRAAPARDpuuqqq+z11193v0+ZMsUuuOCCaB9SgfXxxx/bmWee6X7v3r27TZ06NdqHBPjOmjVr7Nhjj3W/N2vWzL799ttoH1KBR+2LLiaB5HOLFi2yIkWKWNeuXYPBTV2nGV3q1KlzxPt1USGUc845xzZs2GBdunSx/EqvJ9aLSuvWrd37fOmll0b7UICYk1FNU30Sr6Z98cUXYY+7/fbbrX379mHbZHTRc3jmzZtnH330kRX04NWiRQuLj4+3448/3saNG5dmm+eff969b8WLF7fTTjvNvvrqq7D79+3bZ7fccotVqFDBEhMT7aKLLrJNmzaFbfOPf/zDTj75ZPc8zZs3T/Mc1L7oIgDmc6+88orddttt9umnn9r69evt2WefdR8o7yKvvfZa8Ppnn30Wdv/pp59u1113XdhttWrVco/Th7Zq1aruJ3JPXFyce59LlCgR7UMBYpJ3Mhp6eeutt4L3K6Tcc889GT5+8eLFwcdNmjTJ3bZq1argbaqbHgUaXXKTOt4OHjxo0bB69WrXYKCWN7VyKihfe+21Nnv27OA2EydOtDvvvNMefvhhW7JkiWsR7dy5s23evDm4zR133GEffPCBvfvuu/bJJ5+4vz8XXnhhmue75pprrGfPnukeC7UvugiA+VhSUpL7oN50003uA62zuDJlyrgPlHeRsmXLBq8r3IXerw9gyZIlw25Ti2J61DKos+XJkye74qHHqTCoFdKzbds269Wrl9WoUcPdf+KJJ4YVatFZuUKrCk+5cuWsSpUq9vLLL1tycrJdffXVVqpUKXdWOnNm+FfE/fDDD641Umebekzfvn1t69atWXrP9Eeifv367tiOO+44e/DBB+3AgQPB+x955BF3pvrqq6/aMccc457r5ptvtkOHDtmTTz7p3p/KlSvb0KFDw/a7Y8cOV0QrVapkpUuXtrPOOsu+++674P36Xe+ZXpvu11nx119/naVjB/zKOxkNvah2eK6//nrXAjhjxox0H6/Ppfe48uX/nkShz7F3m+pmRtQ6qCEwgwcPDn6+b7zxRtsfslzV4cOHbfjw4a4LWWFGdfG9994La3FT7VRN81rEPv/886PWBYXVxo0bu+3VGvf000+HHZtuGzZsmAtZ2odq1ksvvXTE9/LFF190x6l9nXDCCXbrrbfaxRdfbCNHjgxu88wzz7iGAdXjRo0auceoZqouys6dO13jg7ZTrdNxq6Fh4cKFYS2x//znP10roWotYg8BMB975513rGHDhtagQQPr06eP+3DmxZDO+++/3+666y539qgwpcDnnc2qW0DFYPr06S6wqTArqKXuPtD4wooVK7rbFQYVYi+55BLXJaAzzk6dOrnH7dmzJxiwVGhOOukkVyBnzZrluhtCuw4UgI/2Besqktruxx9/dGf9Cp6hhU9+/fVXV6j1HAqvKnQK2H/++ac7033iiSfsgQcesC+//DL4GB27zo71uG+++cZ1r5x99tn2119/ufsvv/xyq1mzpmuJ0P2DBg1y34QCIOcUaBTK7r33XhfGIk1dwitWrHBBTjVBJ8EKhB6Fv/Hjx7ugtHz5ctc6ppqsehFKn/vHH3/c7atp06ZHrAu6rvp22WWX2bJly9zJqU5YU3fXKsidcsoptnTpUneyqlqq1s3QE+7QLm6dsHfo0CFsH2rd807kFWz13KHbaOUFXfe20f06cQ7dRn+LFEBDGwQQ4zQJBPlT69atA6NGjXK/HzhwIFCxYsXA/Pnzw7bRP/GUKVMy3Ee7du0C/fv3T3P7lVdeGejevXvYbatXr3b7Gzt2bPC25cuXu9tWrFiR4XN07do1MGDAgLDnPOOMM4LXDx48GEhISAj07ds3eNuGDRvcfhctWuSuDxkyJNCpU6ew/a5du9Zts2rVKnd98uTJgQYNGmTp9Y8YMSJw8sknB68//PDDgZIlSwZ27doVvK1z586BOnXqBA4dOhS8Tc8zfPhw9/tnn30WKF26dGDfvn1h+65bt27gX//6l/u9VKlSgXHjxgWOJL33HPA7fS6KFCniakToZejQoe7+2rVrB0aOHBnYvHmz+5yNHz/e3a66plqTmmqk6sL27dvTrW9Lly5N8/zly5cPJCcnB28bM2ZMIDEx0dUEfe5VMxYuXBj2uH79+gV69eoV9pxTp04N2+ZIdaF3796Bjh07ht129913Bxo1ahS8rtfep0+f4PXDhw8HKleu7I7Po7o6aNCg4PV69eoFhg0bFrbf6dOnu+Pbs2dPYN26de731K9Hz33qqae63ydMmBCIi4tLc8wtW7YMDBw4MM3tqqvNmjULZITaFx18kWA+pTM8tZ5phq4ULVrUjbNQa5U38Dm36MzVU61aNfdTrV86A1RXqbok1Dq5bt06dzaZkpLiug8y2oe6nDXmRt3FHnXxevsVdZXMnz/fdcmmphY7tUT26NHDXY5EXebqltBj1IWulkt1vaTuVlFLYeix6BhDv2pOt4Uem/aVetzQ3r173fOIxtOoi/iNN95wZ81qMaxbt+4RjxXA39RNOmbMmLDbvK5cj7pn1TPx0EMPZTjmLLvUpRtawzR2Wp/5tWvXup/qqejYsWPYY1T71GMRSi11oY5UF9RKqJmxodq0aWOjRo1yddYbqhNaS9UDoi7t0LF6apkE0kMAzKcU9BReqlevHrxNDV4aK/Lcc88dcUxLToV2XXpdrl63y4gRI1zXqoqUAl1CQoIb6xc6Xib1Prz9HGm/KrLdunVz3a+peSH0aNQ1oS4Xdd2oy0Pv0dtvv51mXM3Rjs27LfTYdAzqHkpN4y9F3Te9e/d2XePqJtbgaj330QIrAHN1ROOCj0aB6oUXXnCXvKLPv+izrbHPoVJPoNPrCBWJunCk2pQeBcTUs3V1XSfCGr+oYKlLett448r1UzVdQ3O8Gpd6G8Q+AmA+pOCnszoFF42VC6XByhqjovEw0bBgwQJ31qrxL6JC9NNPP7mBxDmhMXUaEK3WObV2ZocGKNeuXduNYfT8/vvvOTou79g2btzojkvHlxG1Uuqi8UEaN6lB0wRAIHLUQ6BxcgpW559/fsT2q1Z+teh7s1U10UHPpUl1aolU0Pvjjz+sXbt2Wd53RnVBEzRUT0PpurbNaKJeZqj1MvVkmblz57rbRRMDNY5b4x699V9Vx3VdE0ZE9yt46jYt/+L1Suk98PaD2MckkHzoww8/tO3bt1u/fv2sSZMmYRd9GNU6GC316tVzxURhS10YN9xwQ5ozyezQTDJNqFCB1IBpda1q2QLNUlN3iKg7XN3QRzo2FSidYevx6gr2utBzQl03KnoqlnPmzHGzpfX6FTQ1YUV/OFQ41UKowKkirtegAg/g6DSMRCdZoZeMVgDQxDO17r/55psRe361dqneavKYwpNa6vSZ1rAQDRdR17MCnCa3qbZoItvo0aODi+mn52h1YcCAAS5gDRkyxJ1Ea1/q3dFzZcUVV1zhJsd41Djw22+/2cCBA23lypWutVRDdnT8oS2pmiCn51Qd18QSb5UG0fur90PbaWiOJoXoPtXBVq1aBffzyy+/uMmC+vfS69XvuqTuEUJ00AKYDyngKXSk182rAKjlSr7//vuwsSF5RbNjVVzUxaoxMyrGCkZaNiAn1NWtAqllXNTqqT8Ias3T+mDe2Dw9R+jsN68bxGsxVIuAipyKrh6vmb1ea0FOqMtFfxQU+FQEt2zZ4rpB2rZtGxw/qOVxVIgVhjX7Wetlhc4iBJAxzchPPdRDqx8owKSmlimFJnWtRopm9OsEUp9p1Q6diIbWDT2fxiBqNrDqn7pF1TNw3333ZbjPo9UFPV7BTGMatX+9/kcffTRsRm9m6KQ3dPyyZkyry1m1UMN1NAt57NixrmZ7NIZSdUzPrfCmpbH0b+CNzRatnqD96m+O3hM9PnXXu8Y3hs6E9sZEai3CI/WWIG/wVXBIl4qMxnfE+rdoHIkKl4qmzqpTD76ORQXhPQfy+1fBaTmV0G+t4HOZ+3iPo4MuYByxq1njXPQzP9E5jYr5Y4895s5Y1TUey/TtLHqfJ0yYEO1DAXxPa5HqgtxH7YsuWgCRLi0jsGvXLve7WtFSz16LZTqTVPDTWBrNRs7tZXFySmNjtGSOqBgyiw6IzuQ673vQNanD+0pMWqdyD7UvugiAAAAAPkMXMAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHyGAAgAAOAzBEAAAACfIQACAAD4DAEQAADAZwiAAAAAPkMABAAA8BkCIAAAgM8QAAEAAHymaLQPAEgtEAjY4m+X2ZSZc239pi2WlJRsxePjrUzpUnZmm9Osa4d2VrJEiWgfJgBExJ69e236vE9s/oIvbeeu3bYvJcUSExOsepVK1qNLR2vZ/EQrVKhQtA8TBUyhgP7aAjFSBKfMnGcT359hq//4M3i7wl5KSoodOnzYXU9MKGnndzrLevU4z+rUqhHFIwaA7Fuzdp29NeVDmzbnP5aUvMfdVqRwYYuPj3f10HNc7Vp26fldrEeXDpz8ImIIgIgJG7dstVsGDbaffltjRYsWtY5tW1vP7udas8YNrWiRIq5VcOPmLTZ5xlx778PZtvWv7RYfF2dPPHCXnf1/p0f78AEgSz76bJHd89hTlrJ/v1UsX84uPq+zXXhuR6tauZJr7Tt46JB9t3ylOyGe++lCO3jwoNU/ro49//jDVrVSxWgfPgoAAiCiTsGu720DbePmra4A3tavryuIGTlw8KDNmPeJDX12jO1L2W/D7r3TzuvYPk+PGQCy68O5H9t9w5+x4vFxdn//m+zcDu2sWNGMR2TphHf0K2+4E+CqlSvaG8+NIAQixwiAiKrkPXus72332M+/rbGbr+ptN15xWabHuixb8ZNdf/eDLgS+NOJRN04GAGKZxjdff/dDLvy9/NQQa9KwfqYepz/VY15/y13qHVfH3hj9hCWULJnrx4uCi1nAiKp3P5jlwp+6P7IS/uTEE+rbM4PvtcOHD9uIF8a6AgkAsUo16snnx7qaNXLwfZkOf6LaeNOVvVytVM3UUBggJwiAiBoVwYnTZrqxfP2vvSJbs9xOP7m5tW3V0lb8/JtrEQSAWPX9ilW28pffrN3pLa3Vyc2y/HjVSNXKuGLFbOL7M10NBbKLAIioWbB4if25fqOdc9b/WdkypbO9n8u6n+t+arA0AMSqiVNnhNWs7FCtVM1cu36DLfx6aQSPDn5DAETUTJkxN8fFUE4/pbkdU6OazZr/mRtTCACxRrVp1sefWe2a1a3Vyc1ztK/Lund1PydPnxOho4MfsRA0oubPDRtd929WxsGkp3DhwtbixMY2ddY8Gz3tOytXoXLEjhEAImH7tk124MBBV6tUs3KiScN6rhv4zw2bInZ88B9aABE1u5KSrVRiQkT25e1nf8r/Fk8FgFiRsu/v2hSJmqexgNrP7uTkCBwZ/IoAiKhR658WQY2E/f/dT5GixSKyPwCIpKL/rU2Rq3kHLL4Y9Q7ZRxcwoqZs6VLuK9927k6yMqUSc7QvfWew9Ot0AgukAog5GzeXtrdfMVu/cXOO96Waqda/esfVjsixwZ9oAUTU/F+rU9y6WNNmf5Tjr5FbuHiJ+5qkKhUrROz4ACBSqlSq6BZwVq3atGVbjvb1/qx57mfb01tG6OjgRwRARE2PLh2tWLGi9s60mTlaxHnSh7Pt0OHDdtkFXbO1liAA5DbVJq14oFo1aXr2F3HW2n+qmaqdqqFAdhEAETUVypW1Tu3OsDVr19lnX3ydrX3s2bvXrYifmFDSunZoF/FjBIBI0XeWJ5Qs4WqWald2fPbl1/b7n+utc/szrHzZMhE/RvgHARBR1ffi861I4cJ23+MjXRDMioOHDtldg590X5R+SbcuVrJEiVw7TgDIKdWoS7t1sS3b/nK1SzUsKzRm+v7HR7ma2eei83PtOOEPBEBEVeMG9ez+22+ynbt229W332s//vRLph6ns+d/3P+YOxvWQtC39euT68cKADl127V9Xc1S7er/wGOZbglcvuoXu+aO+1ytfOCOm13tBHKiUCAng6+ACBn/7lQb8cIrbnHTLme3tV4XdE23wG3bvsN9g8jEaTNs4+atbkX9kYPvdV3AAJAfJCXvsdsfGmZfLvnOqlWp5FoFe5zb0Q2LSe2HlT/Z2+/PsJkffWoHDh60u266xq645IKoHDcKFgIgYsa8TxfaUy++auv+u7q9Vrtv3vgEF+5SUva7bw6Zv/ArO3jwoJUoHm8XndfZ7rz+KivGWlgA8pkDBw7YMy+Nc5PY9u5LsaJFi9qZrU+1mtWqWnx8nAuJS3/40bX8SY1qVeyuG6+xDm1bR/vQUUAQABFTNMNtweIlNvH9GfbpF1+nmR1cp1YNN5OuW+ezrHRiztYOBIBo25WUZNNm/cf1aqQeB62Zw21bnWI9u59rbVq2yPFXyAGhCIDIkquuuspef/119/uUKVPsggtyrytCkzs2bdlqu5P2WPH4OCtTupQLgNFY6kWve8eOHTZ16tQc7cc79jJlyrj9AYgNa9assWOPPdb93qxZM/v222/z9Pn1p1gBUGP89qXst1KJJd3agRXLl7OCaty4cXb11Ve73/v372+jRo2K9iH5CqcTBdSiRYusSJEi1rVr12CAUfjI6FKnTp0j3q+LCqScc845tmHDBuvSpUuuvgYVPo0DbHVyM2ve5AQ79piaEQt/2k9Ow1x26H2jyAGZl1HtUh0Sr3Z98cUXYY+7/fbbrX379mHbZHTRc3jmzZtnH32Us8Xps0PHoRqnWqeap9qXW+Hv448/thYtWlh8fLwdf/zxLoil9vzzz7v3rXjx4nbaaafZV199FXb/vn377JZbbrEKFSpYYmKiXXTRRbZp09/Ddzx//PGH+xtUsmRJq1y5st19991uCI+nZ8+eriaefvrpufI6cWQEwALqlVdesdtuu80+/fRTW79+vT377LPug+Zd5LXXXgte/+yzz8Lu1wfyuuuuC7utVq1a7nEqGlWrapxKfJRfZf6j902tfwAyzzvpDL289dZbwfsVUu65554MH7948eLg4yZNmuRuW7VqVfA21UePAo0uud3aFxqE8tLq1atdKDvzzDNdK6eC8rXXXmuzZ/9vceqJEyfanXfeaQ8//LAtWbLEtYh27tzZNm/+39fY3XHHHfbBBx/Yu+++a5988on7O3PhhRcG7z906JB7Hn1P+8KFC13PkYLmQw89FNymRIkSribGxcXl4TsADwGwAEpKSnIf4Jtuusl9APWhU+jQB827SNmyZYPXFe5C79cHUmdtobepRTE9ahnU2evkyZNdUdHjVDDUCunZtm2b9erVy2rUqOHuP/HEE8MKuOhsXaFVBalcuXJWpUoVe/nlly05Odl1E5QqVcqdrc6cOTPscT/88INrjdRZqB7Tt29f27p1a5bes2XLltlZZ53lCpKK//XXX+/ex9Seeuopq1atmttGZ78ayO3R2fKwYcPsmmuuccd6zDHH2EsvvZSl4wCQlnfSGXpRjfDo86oWwBkzZqT7+EqVKgUfV758eXebWqS82450UqbWQQ11GTx4sNtP6dKl7cYbb3TBJnTs8vDhw10XsmqI6t97770X1uKmGqnadfLJJ7vX8/nnn9t3333naqbqhfar+77++n+L4iusNm7c2G2v+vL000+HHVt2as6LL77ojlP7OuGEE+zWW2+1iy++2EaOHBnc5plnnnENAKq7jRo1co9R3X711Vfd/Tt37nSNDNpOdVPHrQYFBT2vJXbOnDn2448/2r///W9r3ry5q9FDhgxxLYuh7x2ihwBYAL3zzjvWsGFDa9CggfXp08d9aPNiqOf9999vd911lzurrF+/vgt83lmuugtUJKZPn+4Cmwq2glrqbgWdJVasWNHdrjCoEHvJJZdY69at3Zlop06d3OP27Nnjttc4OhWgk046yRXOWbNmuW6ISy+9NLhPBeAjdR0rYOrsVn9Q1FKgM1p1A6kwhpo/f779+uuv7qd3Npu6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", + "text/html": [ + "\n", + "
\n", + "
\n", + " Figure\n", + "
\n", + " \n", + "
\n", + " " + ], + "text/plain": [ + "Canvas(toolbar=Toolbar(toolitems=[('Home', 'Reset original view', 'home', 'home'), ('Back', 'Back to previous …" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.figure()\n", + "parsed = TypeQLVisitor.parse_and_visit(_typeql_query_string)\n", + "query_graph = QueryGraph(parsed)\n", + "answer_graph = AnswerGraph.build(query_graph, _typeql_result)\n", + "plot_instance_4 = answer_graph.plot_with_visualiser(MyVisualisationBuilder())\n", + "# We can also call `visualise(_typeql_query_string, _typeql_result, MyVisualisationBuilder())`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec3be1dc-dd31-4ec5-8fb6-6eb064917de8", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/typedb_jupyter/connection.py b/src/typedb_jupyter/connection.py index 326dbd8..7173c2c 100644 --- a/src/typedb_jupyter/connection.py +++ b/src/typedb_jupyter/connection.py @@ -19,131 +19,76 @@ # under the License. # -from typedb.client import TypeDB -from typedb.api.connection.session import SessionType -from typedb_jupyter.exception import ArgumentError - +from typedb.driver import TypeDB, DriverOptions +from typedb_jupyter.exception import ArgumentError, ConnectionError class Connection(object): current = None - connections = dict() - def __init__(self, client, address, database, credential, alias, create_database): + def __init__(self, driver, address, credential, tls_enabled): self.address = address - self.database = database - self.name = "{}@{}".format(database, address) - - if alias is None: - self.alias = self.name - self.verbose_name = self.name - else: - self.alias = alias - self.verbose_name = "{} ({})".format(self.alias, self.name) - - if client is TypeDB.core_client: - self.client = TypeDB.core_client(address) - elif client is TypeDB.cluster_client: - self.client = TypeDB.cluster_client(address, credential) + if driver is TypeDB.core_driver: + self.driver = TypeDB.core_driver(address, credential, DriverOptions(tls_enabled)) + elif driver is TypeDB.cloud_driver: + self.driver = TypeDB.cloud_driver(address, credential, DriverOptions(tls_enabled)) else: raise ValueError("Unknown client type. Please report this error.") - - if not self.client.databases().contains(database): - if create_database: - self.client.databases().create(database) - print("Created database: {}".format(self.database)) - else: - raise ArgumentError("Database with name '{}' does not exist and automatic database creation has been disabled.".format(database)) - - self.session = self.client.session(database, SessionType.DATA) - self.connections[self.name] = self + self.active_transaction = None def __del__(self): - try: - self.session.close() - finally: - self.client.close() + if self.active_transaction is not None: + self.active_transaction.close() + self.active_transaction = None + self.driver.close() @classmethod - def _get_aliases(cls): - return [cls.connections[name].alias for name in cls.connections] - - @classmethod - def _get_current(cls): - if len(cls.connections) == 0: - raise ArgumentError("No database connection exists. Use -a and -d to specify server address and database name.") - elif cls.current is None: - raise ArgumentError("Current connection was closed. Use -l to list connections and -n to select connection.") - - return cls.current - - @classmethod - def _get_by_alias(cls, alias): - try: - return {cls.connections[name].alias: cls.connections[name] for name in cls.connections}[alias] - except KeyError: - raise ArgumentError("Connection name not recognised. Use -l to list connections.") - - @classmethod - def open(cls, client, address, database, credential, alias, create_database): - if "{}@{}".format(database, address) in cls.connections: - raise ArgumentError("Cannot open more than one connection to the same database. Use -c to close opened connection first.") - elif alias in cls._get_aliases(): - raise ArgumentError("Cannot open more than one connection with the same alias. Use -c to close opened connection first.") - else: - cls.current = Connection(client, address, database, credential, alias, create_database) - print("Opened connection: {}".format(cls.current.verbose_name)) - - @classmethod - def select(cls, alias): - cls.current = cls._get_by_alias(alias) - print("Selected connection: {}".format(cls.current.verbose_name)) - - @classmethod - def get(cls, alias=None): - if alias is None: - return cls._get_current() + def open(cls, client, address, credential, tls_enabled): + if cls.current is None: + cls.current = Connection(client, address, credential, tls_enabled) + print("Opened connection to: {}".format(cls.current.address)) else: - return cls._get_by_alias(alias) - + raise ArgumentError("Cannot open more than one connection. Use `connection close` to close opened connection first.") @classmethod - def display(cls): - print("Current connection: {}".format(cls._get_current().verbose_name)) + def get(cls): + return cls.current @classmethod - def list(cls): - if len(cls.connections) == 0: - print("No open connections.") + def close(cls): + connection = cls.current + cls.current = None + del connection + print("Closed connection") + + def _ensure_transaction_open(self): + if self.active_transaction is None: + raise ArgumentError("There is no open transaction") + elif not self.active_transaction.is_open(): + self.active_transaction = None + raise ConnectionError("The transaction has been closed") + + def get_active_transaction(self): + self._ensure_transaction_open() + return self.active_transaction + + def open_transaction(self, database, transaction_type): + if self.active_transaction is not None: + raise ArgumentError("Cannot open a transaction when there is one active. Please close it first.") else: - print("Open connections:") - for name in sorted(cls.connections): - if cls.connections[name] == cls.current: - prefix = " * " - else: - prefix = " " - - print("{}{}".format(prefix, cls.connections[name].verbose_name)) + self.active_transaction = self.driver.transaction(database, transaction_type) - @classmethod - def set_session(cls, session_type, alias=None): - connection = cls.get(alias) - if connection.session.session_type() != session_type: - connection.session.close() - connection.session = connection.client.session(connection.database, session_type) - - @classmethod - def close(cls, alias=None, delete=False): - connection = cls.get(alias) - verbose_name = connection.verbose_name - if cls.current is not None and cls.current.alias == alias: - cls.current = None + def close_transaction(self): + self._ensure_transaction_open() + self.active_transaction.close() + self.active_transaction = None - connection = cls.connections[connection.name] + def commit_transaction(self): + self._ensure_transaction_open() + self.active_transaction.commit() + self.active_transaction = None - if delete: - connection.session.close() - connection.client.databases().get(connection.database).delete() - print("Deleted database: {}".format(connection.database)) - del cls.connections[connection.name] - print("Closed connection: {}".format(verbose_name)) + def rollback_transaction(self): + self._ensure_transaction_open() + self.active_transaction.rollback() + self.active_transaction = None diff --git a/src/typedb_jupyter/exception.py b/src/typedb_jupyter/exception.py index 4a3e900..0d2c60e 100644 --- a/src/typedb_jupyter/exception.py +++ b/src/typedb_jupyter/exception.py @@ -25,3 +25,21 @@ class ArgumentError(ValueError): class QueryParsingError(ValueError): pass + +class ConnectionError(BaseException): + pass + + +class CommandParsingError(BaseException): + def __init__(self, what, msg): + BaseException.__init__(self) + self.what = what + self.msg = msg + +def is_typedb_jupyter_exception(err): + return ( + isinstance(err, ArgumentError) or + isinstance(err, ConnectionError) or + isinstance(err, CommandParsingError) or + isinstance(err, QueryParsingError) + ) diff --git a/src/typedb_jupyter/graph/__init__.py b/src/typedb_jupyter/graph/__init__.py new file mode 100644 index 0000000..26b0b31 --- /dev/null +++ b/src/typedb_jupyter/graph/__init__.py @@ -0,0 +1,33 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +def visualise(typeql_query_string, typeql_result, visualiser=None): + from typedb_jupyter.graph.query import QueryGraph + from typedb_jupyter.graph.answer import AnswerGraph + from typedb_jupyter.utils.parser import TypeQLVisitor + + parsed = TypeQLVisitor.parse_and_visit(typeql_query_string) + query_graph = QueryGraph(parsed) + answer_graph = AnswerGraph.build(query_graph, typeql_result) + if visualiser is None: + from .answer import PlottableGraphBuilder + visualiser = PlottableGraphBuilder() + answer_graph.plot_with_visualiser(visualiser) \ No newline at end of file diff --git a/src/typedb_jupyter/graph/answer.py b/src/typedb_jupyter/graph/answer.py new file mode 100644 index 0000000..a0e1929 --- /dev/null +++ b/src/typedb_jupyter/graph/answer.py @@ -0,0 +1,286 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from abc import abstractmethod +from typing import List, Any + +############ +# Vertices # +############ +class AnswerVertex: + _SHAPE = None + _COLOUR = None + def __init__(self, vertex): + self.vertex = vertex + + def iid(self): + return self.vertex.get_iid() + + def type(self): + return self.vertex.get_type().get_label() + + def __str__(self): + return str(self.vertex) + + def __hash__(self): + return self.vertex.__hash__() + + def __eq__(self, other): + return self.vertex.__eq__(other.vertex) + + @classmethod + @abstractmethod + def _default_shape(cls): + return cls._SHAPE + + @classmethod + @abstractmethod + def _default_colour(cls): + return cls._COLOUR + + @abstractmethod + def _default_label(self): + raise NotImplementedError("abstract") + + @classmethod + def trim_iid(cls, iid): + full_iid = str(iid) + thing_id = full_iid[4:] + trimmed = thing_id.lstrip("0") + if len(trimmed) < 2: + return thing_id[-2:] + else: + return trimmed + +class RelationVertex(AnswerVertex): + _SHAPE = "d" + _COLOUR = "yellow" + def __init__(self, relation): + super().__init__(relation) + + def _default_label(self): + trimmed_iid = self.__class__.trim_iid(self.vertex.get_iid()) + return "{}[{}]".format(self.vertex.get_type().get_label(), trimmed_iid) + + +class EntityVertex(AnswerVertex): + _SHAPE = "s" + _COLOUR = "pink" + def __init__(self, entity): + super().__init__(entity) + + def _default_label(self): + trimmed_iid = self.__class__.trim_iid(self.vertex.get_iid()) + return "{}[{}]".format(self.vertex.get_type().get_label(), trimmed_iid) + + +class AttributeVertex(AnswerVertex): + _SHAPE = "o" + _COLOUR = "green" + + def __init__(self, attribute): + super().__init__(attribute) + + def _default_label(self): + return "{}:{}".format(self.vertex.get_type().get_label(), self.vertex.get_value()) + + def iid(self): + return self.vertex.get_value() + +######### +# Edges # +######### +class AnswerEdge: + def __init__(self, lhs: AnswerVertex, rhs: AnswerVertex): + self.lhs = lhs + self.rhs = rhs + + @abstractmethod + def _default_label(self): + raise NotImplementedError("abstract") + + def __str__(self): + return "{}--[{}]-->{}".format(self.lhs, self._default_label(), self.rhs) + +class HasEdge(AnswerEdge): + def _default_label(self): + return "has" + + +class LinksEdge(AnswerEdge): + def __init__(self, lhs: AnswerVertex, rhs: AnswerVertex, role): + super().__init__(lhs, rhs) + self.role = role + + def role(self): + self.role.get_label() + + def _default_label(self): + return self.role.get_label().split(":")[1] + +########## +# Graphs # +########## +class IGraphVisualisationBuilder: + + @abstractmethod + def __init__(self): + raise NotImplementedError("abstract") + + def notify_start_next_answer(self, index: int): + pass + + @abstractmethod + def add_entity_vertex(self, answer_index: int, vertex: EntityVertex): + raise NotImplementedError("abstract") + + @abstractmethod + def add_relation_vertex(self, answer_index: int, vertex: RelationVertex): + raise NotImplementedError("abstract") + + @abstractmethod + def add_attribute_vertex(self, answer_index: int, vertex: AttributeVertex): + raise NotImplementedError("abstract") + + @abstractmethod + def add_has_edge(self, answer_index: int, edge: HasEdge): + raise NotImplementedError("abstract") + + @abstractmethod + def add_links_edge(self, answer_index: int, edge: LinksEdge): + raise NotImplementedError("abstract") + + @abstractmethod + def plot(self) -> Any: + raise NotImplementedError("abstract") + + +class AnswerGraph: + def __init__(self, edges: List[List[AnswerEdge]]): + self.edges = edges + + @classmethod + def build(cls, query_graph, answers): + builder = AnswerGraphBuilder(query_graph) + for row in answers: + builder._add_answer_row(row) + return AnswerGraph(builder.answer_edges) + + def plot(self): + return self.plot_with_visualiser(PlottableGraphBuilder()) + + def plot_with_visualiser(self, visualiser: IGraphVisualisationBuilder): + for (index, edge_list) in enumerate(self.edges): + visualiser.notify_start_next_answer(index) + for edge in edge_list: + self._plot_vertex(visualiser, index, edge.lhs) + self._plot_vertex(visualiser, index, edge.rhs) + self._plot_edge(visualiser, index, edge) + return visualiser.plot() + + def _plot_vertex(self, visualiser: IGraphVisualisationBuilder, index: int, vertex: AnswerVertex): + if isinstance(vertex, EntityVertex): + visualiser.add_entity_vertex(index, vertex) + elif isinstance(vertex, RelationVertex): + visualiser.add_relation_vertex(index, vertex) + elif isinstance(vertex, AttributeVertex): + visualiser.add_attribute_vertex(index, vertex) + else: + raise ValueError(f"Unknown vertex type: {vertex}") + + def _plot_edge(self, visualiser: IGraphVisualisationBuilder, index: int, edge: AnswerEdge): + if isinstance(edge, HasEdge): + visualiser.add_has_edge(index, edge) + elif isinstance(edge, LinksEdge): + visualiser.add_links_edge(index, edge) + else: + raise ValueError(f"Unknown edge type: {edge}") + + +class AnswerGraphBuilder: + def __init__(self, query_graph): + self.query_graph = query_graph + self.answer_edges = [] + + # + # @classmethod + # def _filter_visualisable_edges(cls, query_graph): + # query_graph # TODO + + def _add_answer_row(self, row): + this_answer_edges = [] + for query_edge in self.query_graph.edges: + this_answer_edges.append(query_edge.get_answer_edge(row)) + self.answer_edges.append(this_answer_edges) + + +class PlottableGraphBuilder(IGraphVisualisationBuilder): + def __init__(self): + self.edges = [] + self.edge_labels = {} + self.node_shapes = {} + self.node_colours = {} + self.node_labels= {} + + def _add_edge_defaults(self, edge: AnswerEdge): + self.edges.append((edge.lhs, edge.rhs)) + self.edge_labels[(edge.lhs, edge.rhs)] = edge._default_label() + + def _add_vertex_defaults(self, vertex: AnswerVertex): + self.node_shapes[vertex] = vertex._SHAPE + self.node_colours[vertex] = vertex._COLOUR + self.node_labels[vertex] = vertex._default_label() + + + def add_entity_vertex(self, answer_index: int, vertex: EntityVertex): + self._add_vertex_defaults(vertex) + + def add_relation_vertex(self, answer_index: int, vertex: RelationVertex): + self._add_vertex_defaults(vertex) + + def add_attribute_vertex(self, answer_index: int, vertex: AttributeVertex): + self._add_vertex_defaults(vertex) + + def add_has_edge(self, answer_index: int, edge: HasEdge): + self._add_edge_defaults(edge) + + def add_links_edge(self, answer_index: int, edge: LinksEdge): + self._add_edge_defaults(edge) + + def plot(self): + from netgraph import InteractiveGraph + return InteractiveGraph( + self.edges, + edge_labels=self.edge_labels, + node_shape=self.node_shapes, + node_color=self.node_colours, + node_labels=self.node_labels, + arrows=True, + node_label_offset=0.075 + ) + +if __name__ == "__main__": + import matplotlib.pyplot as plt + from netgraph import InteractiveGraph + graph_data = [("a", "b"), ("b", "c")] + node_shapes = { "a" : "o", "b" : "s", "c": "o"} + plot_instance = InteractiveGraph(graph_data, node_shape=node_shapes) + plt.show() diff --git a/src/typedb_jupyter/graph/query.py b/src/typedb_jupyter/graph/query.py new file mode 100644 index 0000000..50b2070 --- /dev/null +++ b/src/typedb_jupyter/graph/query.py @@ -0,0 +1,99 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from abc import abstractmethod + +from typedb_jupyter.utils.ir import Var, Label, Literal, Comparator, \ + Isa, Has, Links, IsaType, AttributeLabelValue, Comparison, Assign +from typedb_jupyter.graph.answer import HasEdge, LinksEdge, \ + EntityVertex, RelationVertex, AttributeVertex + +class QueryGraphEdge: + def __init__(self, lhs: Var, rhs: Var): + self.lhs = lhs + self.rhs = rhs + + + @abstractmethod + def get_answer_edge(self, row): + raise NotImplementedError("abstract") + + @abstractmethod + def __str__(self): + raise NotImplementedError("abstract") + +class QueryHasEdge(QueryGraphEdge): + def __init__(self, lhs, rhs): + super().__init__(lhs, rhs) + + def get_answer_edge(self, row): + owner = row.get(self.lhs.name) + if owner.is_entity(): + lhs = EntityVertex(owner) + else: + assert owner.is_relation() + lhs = RelationVertex(owner) + + assert row.get(self.rhs.name).is_attribute() + rhs = AttributeVertex(row.get(self.rhs.name)) + return HasEdge(lhs, rhs) + + def __str__(self): + return "{}({}, {})".format(self.__class__.__name__, self.lhs, self.rhs) + + +class QueryLinksEdge(QueryGraphEdge): + def __init__(self, lhs, rhs, role): + super().__init__(lhs, rhs) + self.role = role + + def get_answer_edge(self, row): + assert row.get(self.lhs.name).is_relation() + rhs = RelationVertex(row.get(self.lhs.name)) + role = row.get(self.role.name) + + player = row.get(self.rhs.name) + if player.is_entity(): + lhs = EntityVertex(player) + else: + assert player.is_relation() + lhs = RelationVertex(player) + + return LinksEdge(lhs, rhs, role) + + def __str__(self): + return "{}({}, {}, {})".format(self.__class__.__name__, self.lhs, self.rhs, self.role) + +class QueryGraph: + def __init__(self, query: "typedb_jupyter.utils.ir.Match"): + self.edges = lazy_query_graph(query.constraints) + + + +def lazy_query_graph(constraints): + edges = [] + for c in constraints: + if isinstance(c, Has): + edges.append(QueryHasEdge(c.lhs, c.rhs)) + elif isinstance(c, Links): + edges.append(QueryLinksEdge(c.lhs, c.rhs, c.role)) + return edges + diff --git a/src/typedb_jupyter/magic.py b/src/typedb_jupyter/magic.py index 94d7427..d76dbd0 100644 --- a/src/typedb_jupyter/magic.py +++ b/src/typedb_jupyter/magic.py @@ -24,34 +24,10 @@ from traitlets import Bool from IPython.core.magic import Magics, cell_magic, line_magic, magics_class, needs_local_scope from IPython.core.magic_arguments import argument, magic_arguments, parse_argstring -from typedb.api.connection.credential import TypeDBCredential -from typedb.client import TypeDB from typedb_jupyter.connection import Connection -from typedb_jupyter.query import Query from typedb_jupyter.exception import ArgumentError, QueryParsingError - -def substitute_vars(query, local_ns): - try: - query_vars = "".join(query.split("\"")[::2]).replace("{", "}").split("}")[1::2] - except IndexError: - return query - - for var in query_vars: - try: - val = local_ns[var] - except KeyError: - raise QueryParsingError("No variable found in local namespace with name: {}".format(var)) - - if type(val) is str: - val = "\"{}\"".format(val.replace("\"", "'")) - else: - val = str(val) - - query = query.replace("{" + var + "}", val) - - return query - +import typedb_jupyter.subcommands as subcommands @magics_class class TypeDBMagic(Magics, Configurable): @@ -61,56 +37,25 @@ class TypeDBMagic(Magics, Configurable): help="Create database when opening a connection if it does not already exist." ) + @line_magic("typedb") - @magic_arguments() - @argument("-a", "--address", type=str, help="TypeDB server address for new connection.") - @argument("-d", "--database", type=str, help="Database name for new connection.") - @argument("-u", "--username", type=str, help="Username for new Cloud/Cluster connection.") - @argument("-p", "--password", type=str, help="Password for new Cloud/Cluster connection.") - @argument("-c", "--certificate", type=str, help="TLS certificate path for new Cloud/Cluster connection.") - @argument("-n", "--alias", type=str, help="Custom alias for new connection, or alias of existing connection to select.") - @argument("-l", "--list", action="store_true", help="List currently open connections.") - @argument("-k", "--close", type=str, help="Close a connection by name.") - @argument("-x", "--delete", type=str, help="Close a connection by name and delete its database.") def execute(self, line=""): - args = parse_argstring(self.execute, line) - - if args.list: - return Connection.list() - elif args.delete: - return Connection.close(args.delete, delete=True) - elif args.close: - return Connection.close(args.close) - else: - cluster_args = (args.username, args.password, args.certificate) - - if args.database is None: - if args.address is not None or not all(arg is None for arg in cluster_args): - raise ArgumentError("Cannot open connection without a database name. Use -d to specify database.") - elif args.alias is None: - Connection.display() - else: - Connection.select(args.alias) + args = line.split(" ") + if len(args) > 0: + command_name = args[0].lower() + if command_name in subcommands.AVAILABLE_COMMANDS: + subcommand = subcommands.AVAILABLE_COMMANDS[args[0]] else: - if all(arg is None for arg in cluster_args): - client = TypeDB.core_client - credential = None - elif all(arg is not None for arg in cluster_args): - client = TypeDB.cluster_client - credential = TypeDBCredential(args.username, args.password, args.certificate) - else: - raise ArgumentError("Cannot open cluster connection without a username, password, and certificate path. Use -u, -p, and -c to specify these.") - - if args.alias is not None and not re.fullmatch(r"[a-zA-Z0-9-_]+", args.alias): - raise ArgumentError("Custom aliases can only contains alphanumeric characters, hyphens, and underscores.") - - if args.address is None: - address = TypeDB.DEFAULT_ADDRESS - else: - address = args.address - - Connection.open(client, address, args.database, credential, args.alias, self.create_database) - return + print("Unrecognised command: ", args[0]) + subcommand = subcommands.Help + else: + subcommand = subcommands.Help + + try: + return subcommand.execute(args[1:]) + except subcommands.CommandParsingError as err: + print("Exception with subcommand: ", err.msg) + return err def __init__(self, shell): Configurable.__init__(self, config=shell.config) @@ -130,7 +75,7 @@ class TypeQLMagic(Magics, Configurable): strict_transactions = Bool( False, config=True, - help="Require session and transaction types to be specified for every transaction." + help="Require transaction types to be specified for every transaction." ) global_inference = Bool( False, @@ -138,46 +83,34 @@ class TypeQLMagic(Magics, Configurable): help="Enable rule inference for all queries. Can be overridden per query with -i." ) + QUERY_RESULT_VARIABLE = "_typeql_result" + QUERY_STRING_VARIABLE = "_typeql_query_string" + @needs_local_scope - @line_magic("typeql") @cell_magic("typeql") @magic_arguments() - @argument("line", default="", nargs="*", type=str, help="Valid TypeQL string.") - @argument("-r", "--result", type=str, help="Assign query result to the named variable instead of printing.") - @argument("-f", "--file", type=str, help="Read in query from a TypeQL file at the specified path.") - @argument("-i", "--inference", type=bool, help="Enable (True) or disable (False) rule inference for query.") - @argument("-s", "--session", type=str, help="Force a particular session type for query, 'schema' or 'data'.") - @argument("-t", "--transaction", type=str, help="Force a particular transaction type for query, 'read' or 'write'.") def execute(self, line="", cell="", local_ns=None): if local_ns is None: local_ns = {} args = parse_argstring(self.execute, line) - query = " ".join(args.line) + "\n" + cell - query = substitute_vars(query, local_ns) + query = cell # Save globals and locals, so they can be referenced in bind vars user_ns = self.shell.user_ns.copy() user_ns.update(local_ns) - if args.file: - with open(args.file, "r") as infile: - query = infile.read() + "\n" + query - - if query.strip() == "": + if not query.strip(): raise ArgumentError("No query string supplied.") connection = Connection.get() - query = Query(query, args.session, args.transaction, args.inference, self.strict_transactions, self.global_inference) - result = query.run(connection, self.show_info) - - if args.result: - print("Returning data to local variable: '{}'".format(args.result)) - self.shell.user_ns.update({args.result: result}) - return + tx = connection.get_active_transaction() + answer_type, answer = self._run_query(tx, query) + self._print_answers(answer_type, answer) - # Return results into the default ipython _ variable - return result + self.shell.user_ns.update({self.QUERY_RESULT_VARIABLE: answer}) + self.shell.user_ns.update({self.QUERY_STRING_VARIABLE: query}) + return answer def __init__(self, shell): Configurable.__init__(self, config=shell.config) @@ -185,3 +118,32 @@ def __init__(self, shell): # Add ourselves to the list of module configurable via %config self.shell.configurables.append(self) + + def _run_query(self, transaction, query): + from typedb.concept.answer.concept_row_iterator import ConceptRowIterator + from typedb.concept.answer.concept_document_iterator import ConceptDocumentIterator + from typedb.concept.answer.ok_query_answer import OkQueryAnswer + answer = transaction.query(query).resolve() + if answer.is_concept_rows(): + return (ConceptRowIterator, list(answer.as_concept_rows())) + elif answer.is_concept_documents(): + return (ConceptDocumentIterator, list(answer.as_concept_documents())) + elif answer.is_ok(): + return (OkQueryAnswer, None) + else: + raise NotImplementedError("Unhandled answer type") + + + def _print_answers(self, answer_type, answer): + from typedb.concept.answer.concept_row_iterator import ConceptRowIterator + from typedb.concept.answer.concept_document_iterator import ConceptDocumentIterator + from typedb.concept.answer.ok_query_answer import OkQueryAnswer + from typedb_jupyter.utils.display import print_rows, print_documents + if answer_type == OkQueryAnswer: + print("Query completed successfully! (No results to show)") + elif answer_type == ConceptDocumentIterator: + print_documents(answer) + elif answer_type == ConceptRowIterator: + print_rows(answer) + else: + raise NotImplementedError("Unhandled answer type") diff --git a/src/typedb_jupyter/query.py b/src/typedb_jupyter/query.py deleted file mode 100644 index 01aecc1..0000000 --- a/src/typedb_jupyter/query.py +++ /dev/null @@ -1,298 +0,0 @@ -# -# Copyright (C) 2023 Vaticle -# -# Licensed to the Apache Software Foundation (ASF) under one -# or more contributor license agreements. See the NOTICE file -# distributed with this work for additional information -# regarding copyright ownership. The ASF licenses this file -# to you under the Apache License, Version 2.0 (the -# "License"); you may not use this file except in compliance -# with the License. You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, -# software distributed under the License is distributed on an -# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -# KIND, either express or implied. See the License for the -# specific language governing permissions and limitations -# under the License. -# - -import math -from typedb.client import TypeDBOptions -from typedb.api.connection.session import SessionType -from typedb.api.connection.transaction import TransactionType -from typedb.concept.answer.concept_map import ConceptMap -from typedb.concept.answer.concept_map_group import ConceptMapGroup -from typedb.concept.answer.numeric import Numeric -from typedb.concept.answer.numeric_group import NumericGroup -from typedb_jupyter.connection import Connection -from typedb_jupyter.exception import ArgumentError, QueryParsingError - - -class Query(object): - def __init__(self, query, session_arg, transaction_arg, inference_arg, strict_transactions, global_inference): - self.query = query - self.query_type = self._get_query_type(self.query) - self.session_type = self._get_session_type(self.query_type, session_arg, strict_transactions) - self.transaction_type = self._get_transaction_type(self.query_type, transaction_arg, strict_transactions) - - if inference_arg is None: - self.infer = global_inference - else: - self.infer = inference_arg - - @staticmethod - def _get_query_args(query): - # Warning: This method is experimental and not guaranteed to always function correctly. Copy at your own risk. - - in_escape = False - in_literal = False - in_comment = False - literal_delimiter = None - arg_string = "" - - for char in query: - if in_escape: - in_escape = False - arg_string += " " - continue - - if in_literal and char == "\\": - in_escape = True - arg_string += " " - continue - - if not in_comment and char in ("\"", "'"): - if not in_literal: - in_literal = True - literal_delimiter = char - arg_string += " " - continue - if in_literal and char == literal_delimiter: - in_literal = False - arg_string += " " - continue - - if not in_literal: - if char == "#": - in_comment = True - arg_string += " " - continue - if in_comment and char == "\n": - in_comment = False - arg_string += " " - continue - - if not in_literal and not in_comment: - if char in (",", ";"): - arg_string += " " - else: - arg_string += char - - return arg_string.split() - - @staticmethod - def _get_query_type(query): - # Warning: This method is experimental and not guaranteed to always function correctly. Copy at your own risk. - - query_args = Query._get_query_args(query) - - keyword_counts = { - "match": 0, - "get": 0, - "define": 0, - "undefine": 0, - "insert": 0, - "delete": 0, - "group": 0, - "count": 0, - "sum": 0, - "max": 0, - "min": 0, - "mean": 0, - "median": 0, - "std": 0, - } - - for arg in query_args: - if arg in keyword_counts: - keyword_counts[arg] += 1 - - aggregate_count = sum(( - keyword_counts["count"], - keyword_counts["sum"], - keyword_counts["max"], - keyword_counts["min"], - keyword_counts["mean"], - keyword_counts["median"], - keyword_counts["std"], - )) - - candidate_query_types = list() - - if keyword_counts["group"] > 0 and aggregate_count > 0: - candidate_query_types.append("match-group-aggregate") - elif aggregate_count > 0: - candidate_query_types.append("match-aggregate") - elif keyword_counts["group"] > 0: - candidate_query_types.append("match-group") - elif keyword_counts["get"] > 0: - candidate_query_types.append("match") - - if keyword_counts["define"] > 0: - candidate_query_types.append("define") - - if keyword_counts["undefine"] > 0: - candidate_query_types.append("undefine") - - if keyword_counts["insert"] > 0 and keyword_counts["delete"] > 0: - candidate_query_types.append("update") - elif keyword_counts["insert"] > 0: - candidate_query_types.append("insert") - elif keyword_counts["delete"] > 0: - candidate_query_types.append("delete") - - if len(candidate_query_types) > 1: - raise QueryParsingError("Query contains incompatible keywords: '{}'".format("', '".join(candidate_query_types))) - elif len(candidate_query_types) == 1: - return candidate_query_types[0] - elif keyword_counts["match"] > 0: - return "match" - else: - raise QueryParsingError("Query contains no keywords.") - - @staticmethod - def _get_session_type(query_type, session_arg, strict_transactions): - if session_arg is None: - if strict_transactions: - raise ArgumentError("Strict transaction types is enabled and no session type was provided. Use -s to specify session type.") - elif query_type in ("define", "undefine"): - return SessionType.SCHEMA - else: - return SessionType.DATA - else: - if session_arg.lower() == "schema": - return SessionType.SCHEMA - elif session_arg.lower() == "data": - return SessionType.DATA - else: - raise ArgumentError("Incorrect session type provided. Session type must be 'schema' or 'data'.") - - @staticmethod - def _get_transaction_type(query_type, transaction_arg, strict_transactions): - if transaction_arg is None: - if strict_transactions: - raise ArgumentError("Strict transaction types is enabled and no transaction type was provided. Use -t to specify transaction type.") - elif query_type in ("define", "undefine", "insert", "update", "delete"): - return TransactionType.WRITE - else: - return TransactionType.READ - else: - if transaction_arg.lower() == "read": - return TransactionType.READ - elif transaction_arg.lower() == "write": - return TransactionType.WRITE - else: - raise ArgumentError("Incorrect transaction type provided. Transaction type must be 'read' or 'write'.") - - def _get_options(self, connection): - if connection.client.is_cluster(): - return TypeDBOptions().cluster().set_infer(self.infer) - else: - return TypeDBOptions().core().set_infer(self.infer) - - def _print_info(self, connection): - connection_arg = "Connection: {}".format(connection.verbose_name) - - if self.session_type == SessionType.SCHEMA: - session_arg = "Session: schema" - else: - session_arg = "Session: data" - - if self.transaction_type == TransactionType.READ: - transaction_arg = "Transaction: read" - else: - transaction_arg = "Transaction: write" - - query_arg = "Query: {}".format(self.query_type) - - if self.infer: - inference_arg = "Inference: on" - else: - inference_arg = "Inference: off" - - info = "{}\n{}\n{}\n{}\n{}".format( - connection_arg, session_arg, transaction_arg, query_arg, inference_arg - ) - - print(info) - - @staticmethod - def _group_key(concept): - if concept.is_type(): - return str(concept.as_type().get_label()) - elif concept.is_entity(): - return concept.as_entity().get_iid() - elif concept.is_relation(): - return concept.as_relation().get_iid() - elif concept.is_attribute(): - return concept.as_attribute().get_value() - else: - raise ValueError("Unknown concept type. Please report this error.") - - @staticmethod - def _parse_answer(answer, answer_type): - if answer_type is ConceptMap: - return [concept_map.to_json() for concept_map in answer] - elif answer_type is ConceptMapGroup: - return {Query._group_key(map_group.owner()): Query._parse_answer(map_group.concept_maps(), ConceptMap) for map_group in answer} - elif answer_type is Numeric: - if answer.is_int(): - return answer.as_int() - elif answer.is_float(): - return answer.as_float() - else: - return math.nan - elif answer_type is NumericGroup: - return {Query._group_key(numeric_group.owner()): Query._parse_answer(numeric_group.numeric(), Numeric) for numeric_group in answer} - else: - raise ValueError("Unknown answer type. Please report this error.") - - def run(self, connection, show_info): - Connection.set_session(self.session_type) - options = self._get_options(connection) - - if show_info: - self._print_info(connection) - - try: - with connection.session.transaction(self.transaction_type, options) as transaction: - if self.query_type == "match": - results = self._parse_answer(transaction.query().match(self.query), ConceptMap) - elif self.query_type == "match-aggregate": - results = self._parse_answer(transaction.query().match_aggregate(self.query).get(), Numeric) - elif self.query_type == "match-group": - results = self._parse_answer(transaction.query().match_group(self.query), ConceptMapGroup) - elif self.query_type == "match-group-aggregate": - results = self._parse_answer(transaction.query().match_group_aggregate(self.query), NumericGroup) - elif self.query_type == "define": - transaction.query().define(self.query) - elif self.query_type == "undefine": - transaction.query().undefine(self.query) - elif self.query_type == "insert": - transaction.query().insert(self.query) - elif self.query_type == "delete": - transaction.query().delete(self.query) - elif self.query_type == "update": - transaction.query().update(self.query) - - if self.transaction_type == TransactionType.WRITE: - transaction.commit() - print('{} query success.'.format(self.query_type.title())) - return - else: - return results - finally: - Connection.set_session(SessionType.DATA) diff --git a/src/typedb_jupyter/response.py b/src/typedb_jupyter/response.py new file mode 100644 index 0000000..7560c34 --- /dev/null +++ b/src/typedb_jupyter/response.py @@ -0,0 +1,184 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from typedb.concept.answer.concept_row_iterator import ConceptRowIterator +from typedb.concept.answer.concept_document_iterator import ConceptDocumentIterator +from typedb.concept.answer.ok_query_answer import OkQueryAnswer +from typedb_jupyter.exception import ArgumentError + +raise NotImplementedError("Do not import me") + +# class Response(object): +# def __init__(self, query, answer, output_format, transaction): +# self.query = query +# self.output_format = output_format +# self.answer_type = self._get_answer_type(answer) +# self.result, self.message = self._format(self.query, answer, self.answer_type, output_format, transaction) +# +# @staticmethod +# def _get_answer_type(answer): +# if answer.is_concept_rows(): +# return ConceptRowIterator +# elif answer.is_concept_documents(): +# return ConceptDocumentIterator +# elif answer.is_ok(): +# return OkQueryAnswer +# else: +# raise NotImplementedError("Unhandled answer type") +# +# @staticmethod +# def _group_key(concept): +# if concept.is_type(): +# return concept.as_type().get_label().name +# elif concept.is_entity(): +# return concept.as_entity().get_iid() +# elif concept.is_relation(): +# return concept.as_relation().get_iid() +# elif concept.is_attribute(): +# return concept.as_attribute().get_value() +# else: +# raise ValueError("Unknown concept type. Please report this error.") +# +# @staticmethod +# def _format_json(answer, answer_type): +# if answer_type is ConceptRowIterator: +# return [str(concept_row) for concept_row in answer] +# else: +# raise ValueError("Unknown answer type. Please report this error.") +# +# @staticmethod +# def _serialise_concepts(results, transaction): +# concepts = dict() +# binding_counts = dict() +# +# for result in results: +# concept_map = result.map +# +# for binding in concept_map.keys(): +# if not concept_map[binding].is_thing(): +# continue +# +# thing = concept_map[binding].as_thing() +# iid = thing.get_iid() +# +# if iid not in concepts.keys(): +# if binding not in binding_counts.keys(): +# binding_counts[binding] = 1 +# else: +# binding_counts[binding] += 1 +# +# concept = { +# "binding": "{}_{}".format(binding, binding_counts[binding]), +# "object": thing, +# } +# +# concepts[iid] = concept +# +# for concept in concepts.values(): +# concept["type"] = concept["object"].get_type().get_label().name +# +# if concept["object"].is_attribute(): +# concept["root-type"] = transaction.concepts.get_root_attribute_type().get_label().name +# concept["value"] = concept["object"].as_attribute().get_value() +# concept["value-type"] = str(concept["object"].get_type().get_value_type()) +# +# if concept["object"].is_entity(): +# concept["root-type"] = transaction.concepts.get_root_entity_type().get_label().name +# ownerships = [attribute.get_iid() for attribute in concept["object"].get_has(transaction)] +# concept["ownerships"] = [concepts[iid]["binding"] for iid in ownerships if iid in concepts.keys()] +# +# if concept["object"].is_relation(): +# concept["root-type"] = transaction.concepts.get_root_relation_type().get_label().name +# ownerships = [attribute.get_iid() for attribute in concept["object"].get_has(transaction)] +# concept["ownerships"] = [concepts[iid]["binding"] for iid in ownerships if iid in concepts.keys()] +# roleplayers = concept["object"].get_players_by_role_type(transaction) +# concept["roleplayers"] = list() +# +# for role in roleplayers.keys(): +# for roleplayer in roleplayers[role]: +# iid = roleplayer.get_iid() +# +# if iid in concepts.keys(): +# concept["roleplayers"].append((role.get_label().name, concepts[iid]["binding"])) +# +# concept.pop("object") +# +# serial = {concept["binding"]: concept for concept in concepts.values()} +# +# for entry in serial.values(): +# entry.pop("binding") +# +# return serial +# +# @staticmethod +# def _format_typeql(answer, answer_type, transaction): +# if answer_type is ConceptRow: +# concepts = Response._serialise_concepts(answer, transaction) +# lines = list() +# +# for binding, concept in concepts.items(): +# lines.append("${} isa {};".format(binding, concept["type"])) +# +# if "value" in concept.keys(): +# if concept["value-type"] == "string": +# lines.append("${} \"{}\";".format(binding, concept["value"])) +# elif concept["value-type"] == "datetime": +# lines.append("${} {};".format(binding, str(concept["value"]).replace(" ", "T"))) +# else: +# lines.append("${} {};".format(binding, concept["value"])) +# +# if "ownerships" in concept.keys(): +# for attribute_binding in concept["ownerships"]: +# lines.append("${} has ${};".format(binding, attribute_binding)) +# +# if "roleplayers" in concept.keys(): +# if len(concept["roleplayers"]) > 0: +# roleplayers = list() +# +# for roleplayer in concept["roleplayers"]: +# roleplayers.append("{}: ${}".format(roleplayer[0], roleplayer[1])) +# +# lines.append("${} ({});".format(binding, ", ".join(roleplayers))) +# +# return "\n".join(lines) +# elif answer_type in (ConceptDocumentIterator): +# raise NotImplementedError("fetch") +# else: +# raise ValueError("Unknown answer type. Please report this error.") +# +# @staticmethod +# def _format(query, answer, answer_type, output_format, transaction): +# if answer_type is OkQueryAnswer: +# result = None +# message = "Query executed successfully!" +# return result, message +# else: +# if output_format == "json": +# result = Response._format_json(answer, answer_type) +# message = None +# elif output_format == "typeql": +# raise NotImplementedError("typeql output") +# result = Response._format_typeql(answer, answer_type, transaction) +# message = None +# else: +# raise ArgumentError("Unknown output format: '{}'".format(output_format)) +# +# return result, message diff --git a/src/typedb_jupyter/subcommands.py b/src/typedb_jupyter/subcommands.py new file mode 100644 index 0000000..c6b599d --- /dev/null +++ b/src/typedb_jupyter/subcommands.py @@ -0,0 +1,213 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# +import abc +import argparse + +from typedb_jupyter.exception import ArgumentError, CommandParsingError +from typedb.api.connection.transaction import TransactionType + +def parser_exit_override(a, b): + raise CommandParsingError(a, b) + +class SubCommandBase(abc.ABC): + + @classmethod + @abc.abstractmethod + def execute(cls, args): + raise NotImplementedError("abstract") + + @classmethod + def get_parser(cls): + raise NotImplementedError("abstract") + + @classmethod + def help(cls): + cls.get_parser().print_help() + + @classmethod + def name(cls): + return str(cls.get_parser().prog) + + @classmethod + def print_help(cls): + print(cls.get_parser().format_help()) + +class Connect(SubCommandBase): + _PARSER = None + @classmethod + def get_parser(cls): + if cls._PARSER is None: + parser = argparse.ArgumentParser( + prog='connect', + description='Establishes the connection to TypeDB' + ) + parser.exit = parser_exit_override + parser.add_argument("action", choices=["open", "close", "help"]) + parser.add_argument("kind", nargs='?', choices=["core", "cluster"]) + parser.add_argument("address", nargs='?', default="127.0.0.1:1729") + parser.add_argument("username", nargs='?', default = "admin") + parser.add_argument("password", nargs='?', default = "password") + parser.add_argument("--tls-enabled", action="store_true", help="Use for encrypted servers") + cls._PARSER = parser + return cls._PARSER + + @classmethod + def execute(cls, args): + from typedb.driver import TypeDB + from typedb.api.connection.credentials import Credentials + from typedb_jupyter.connection import Connection + + cmd = cls.get_parser().parse_args(args) + if cmd.action == "help": + cls.print_help() + elif cmd.action == "open": + driver = TypeDB.cloud_driver if cmd.kind == "cluster" else TypeDB.core_driver + credential = Credentials(cmd.username, cmd.password) + Connection.open(driver, cmd.address, credential, bool(cmd.tls_enabled)) + elif cmd.action == "close": + Connection.close() + else: + raise NotImplementedError("Unimplemented for action: ", cmd.action) + + +class Database(SubCommandBase): + _PARSER = None + @classmethod + def get_parser(cls): + if cls._PARSER is None: + parser = argparse.ArgumentParser( + prog='database', + description='Database management' + ) + parser.exit = parser_exit_override + parser.add_argument("action", choices=["create", "recreate", "list", "delete", "schema", "help"]) + parser.add_argument("name", nargs='?') + cls._PARSER = parser + return cls._PARSER + + @classmethod + def execute(cls, args): + from typedb_jupyter.connection import Connection + + cmd = cls.get_parser().parse_args(args) + + driver = Connection.get().driver + if cmd.action == "help": + cls.print_help() + elif cmd.action == "create": + driver.databases.create(cmd.name) + print("Created database ", cmd.name) + elif cmd.action == "recreate": + if driver.databases.contains(cmd.name): + driver.databases.get(cmd.name).delete() + driver.databases.create(cmd.name) + print("Recreated database ", cmd.name) + elif cmd.action == "delete": + driver.databases.get(cmd.name).delete() + print("Deleted database ", cmd.name) + elif cmd.action == "list": + print("Databases: ", ", ".join(map(lambda db: db.name, driver.databases.all()))) + elif cmd.action == "schema": + db = driver.databases.get(cmd.name) + print("Schema for database: ", db.name) + print(db.schema()) + else: + raise NotImplementedError("Unimplemented for action: ", cmd.action) + + +class Transaction(SubCommandBase): + _PARSER = None + @classmethod + def get_parser(cls): + if cls._PARSER is None: + parser = argparse.ArgumentParser( + prog='transaction', + description='Opens or closes a transaction to a database on the active connection' + ) + parser.exit = parser_exit_override + parser.add_argument("action", choices=["open", "close", "commit", "rollback", "help"]) + parser.add_argument("database", nargs='?', help="Only for 'open'") + parser.add_argument("tx_type", nargs='?', choices=["schema", "write", "read"], help="Only for 'open'") + cls._PARSER = parser + return cls._PARSER + + TX_TYPE_MAP = { + "schema": TransactionType.SCHEMA, + "write": TransactionType.WRITE, + "read": TransactionType.READ, + } + + @classmethod + def execute(cls, args): + from typedb_jupyter.connection import Connection + + cmd = cls.get_parser().parse_args(args) + + connection = Connection.get() + if cmd.action == "help": + cls.print_help() + elif cmd.action == "open": + if cmd.database is None or cmd.tx_type is None: + raise ArgumentError("transaction open database tx_type") + connection.open_transaction(cmd.database, cls.TX_TYPE_MAP[cmd.tx_type]) + print("Opened {} transaction on database '{}' ".format(cmd.tx_type, cmd.database)) + elif cmd.action == "close": + connection.close_transaction() + print("Transaction closed") + elif cmd.action == "commit": + connection.commit_transaction() + print("Transaction committed") + elif cmd.action == "rollback": + connection.rollback_transaction() + print("Transaction rolled back") + else: + raise NotImplementedError("Unimplemented for action: ", cmd.action) + +class Help(SubCommandBase): + _PARSER = None + + @classmethod + def get_parser(cls): + if cls._PARSER is None: + parser = argparse.ArgumentParser( + prog='help', + description='Shows this help description' + ) + parser.exit = parser_exit_override + cls._PARSER = parser + return cls._PARSER + + @classmethod + def execute(cls, args): + print("Available commands:", ", ".join(AVAILABLE_COMMANDS.keys())) + if not (len(args) > 0 and args[0] == "short"): + for subcommand in AVAILABLE_COMMANDS.values(): + print("-"*80) + print("Help for command '%s':"%subcommand.name()) + subcommand.print_help() + + +AVAILABLE_COMMANDS = { + Connect.name() : Connect, + Database.name() : Database, + Transaction.name(): Transaction, + Help.name(): Help, +} diff --git a/src/typedb_jupyter/utils/__init__.py b/src/typedb_jupyter/utils/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/typedb_jupyter/utils/display.py b/src/typedb_jupyter/utils/display.py new file mode 100644 index 0000000..556393c --- /dev/null +++ b/src/typedb_jupyter/utils/display.py @@ -0,0 +1,48 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +def print_rows(rows): + if len(rows) == 0: + print("Query returned an empty set of rows.") + else: + from IPython.display import HTML, display + print("Query returned {} rows.".format(len(rows))) + headers = list(rows[0].column_names()) + display(HTML( + '{}
{}
'.format( + ''.join(str(_) for _ in headers), + ''.join( + '{}'.format(''.join(str(_) for _ in row.concepts())) for row in rows) + ) + )) + +def print_documents(documents): + if len(documents) == 0: + print("Query returned an empty set of documents.") + else: + from json import dumps + print("Query returned {} documents.".format(len(documents))) + for document in documents: + print(dumps(document, indent=2)) + +# def display_graph(graph): +# for edge in graph: +# \ No newline at end of file diff --git a/src/typedb_jupyter/utils/ir.py b/src/typedb_jupyter/utils/ir.py new file mode 100644 index 0000000..5358155 --- /dev/null +++ b/src/typedb_jupyter/utils/ir.py @@ -0,0 +1,137 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +class Match: + def __init__(self, constraints): + self.constraints = constraints + + def __str__(self): + return "Match(%s)"%(", ".join(str(c) for c in self.constraints)) + +class Label: + def __init__(self, name): + self.name = name + + def __str__(self): + return self.name + +class Var: + _INTERNAL = 0 + def __init__(self, name): + self.name = name.lstrip("$") + + @classmethod + def next_internal(cls): + cls._INTERNAL += 1 + return "$INTERNAL__{}".format(cls._INTERNAL) + + def __str__(self): + return self.name + +class Literal: + def __init__(self, value): + self.value = value + + def __str__(self): + return self.value + +class Comparator: + def __init__(self, symbol): + self.symbol = symbol + + def __str__(self): + return self.symbol + +# Constraints + +class Constraint: + def may_set_lhs(self, lhs: Var): + pass + +class BinaryConstraint(Constraint): + def __init__(self, lhs:Var, rhs:Var): + self.lhs = lhs + self.rhs = rhs + + def may_set_lhs(self, lhs: Var): + assert self.lhs is None + self.lhs = lhs + + def __str__(self): + return "{}({}, {})".format(self.__class__.__name__, self.lhs, self.rhs) + +class Isa(BinaryConstraint): + def __init__(self, lhs: Var, rhs: Var): + super().__init__(lhs, rhs) + +class Has(BinaryConstraint): + def __init__(self, lhs: Var, rhs: Var): + super().__init__(lhs, rhs) + +class Links(BinaryConstraint): + # TODO: role would ideally be Var, but we don't have a rolename keyword + def __init__(self, lhs: Var, rhs: Var, role): + super().__init__(lhs, rhs) + self.role = role + + +# TODO: Deprecate +class IsaType(Constraint): + def __init__(self, lhs: Var, rhs: Label): + self.lhs = lhs + self.rhs = rhs + + def __str__(self): + return "{}({}, {})".format(self.__class__.__name__, self.lhs, self.rhs) + + def may_set_lhs(self, lhs: Var): + if self.lhs is None: + self.lhs = lhs + +class AttributeLabelValue(Constraint): + def __init__(self, lhs: Var, label: Label, value: Literal): + self.lhs = lhs + self.label = label + self.value = value + + def __str__(self): + return "{}({}, {}, {})".format(self.__class__.__name__, self.lhs, self.label, self.value) + +class Comparison(Constraint): + def __init__(self, lhs: Var, rhs: Label, comparator): + self.lhs = lhs + self.rhs = rhs + self.comparator = comparator + + def __str__(self): + return "{}({}, {}, {})".format(self.__class__.__name__, self.lhs, self.comparator, self.rhs) + + +class Assign(Constraint): # Not sub edge + # Treat RHS as black box. + def __init__(self, lhs: Var, rhs): + self.assigned = lhs + self.expr = rhs + + def __str__(self): + return "{}({}, {})".format(self.__class__.__name__, self.lhs, self.rhs) + +# TODO: Add schema edges diff --git a/src/typedb_jupyter/utils/parser.py b/src/typedb_jupyter/utils/parser.py new file mode 100644 index 0000000..0f1bd1f --- /dev/null +++ b/src/typedb_jupyter/utils/parser.py @@ -0,0 +1,179 @@ +# +# Copyright (C) 2023 Vaticle +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +from parsimonious.grammar import Grammar +from parsimonious.nodes import Node, NodeVisitor + +from typedb_jupyter.utils.ir import Match, \ + Var, Label, Literal, Comparator, \ + Isa, Has, Links, IsaType, AttributeLabelValue, Comparison, Assign + + +def flatten(l): + flat = [] + for sl in l: + if isinstance(sl, list): + flat = flat + flatten(sl) + else: + flat.append(sl) + return flat + + +def non_null(l): + return [e for e in l if e is not None] + +class TypeQLVisitor(NodeVisitor): + GRAMMAR = Grammar(""" + query = ws match_clause ws + + match_clause = "match" ws (pattern ";" ws)+ + + pattern = native / assign / comparison + assign = "TODO" + + comparison = (var/literal) ws comparator ws (var/literal) ws + comparator = "=" / ">" / ">=" / "<" / "<=" / "!=" / "like" / "contains" + + native = var ws constraint ws ( "," ws constraint ws)* + constraint = has_labelled / has / links / isa + + has_labelled = "has" ws label ws (var / literal) + has = "has" ws var + + links = "links" ws "(" ws role_player ws ( "," ws role_player ws)* ")" + isa = "isa" ws (label/var) + + role_player = (var/label) ws ":" ws var + + label = ~"[A-Za-z0-9_\-]+" + identifier = ~"[A-Za-z0-9_\-]+" + var = ~"\$[A-Za-z0-9_\-]+" + literal = (integer_literal / string_literal) + integer_literal = ~"[0-9]+" + string_literal = ~'"[^\"]+"' + ignored = ~"[^']+" + ws = ~"\s*" + """) + + @classmethod + def parse_and_visit(cls, input: str): + tree = TypeQLVisitor.GRAMMAR.parse(input) + visitor = TypeQLVisitor() + return visitor.visit(tree)[1] + + def visit_query(self, node:Node, visited_children): + return non_null(flatten(visited_children[1])) + + def visit_ws(self, node:Node, visited_children): + return + + def visit_var(self, node:Node, visited_children): + return Var(node.text) + + def visit_label(self, node:Node, visited_children): + return Label(node.text) + + def visit_identifier(self, node:Node, visited_children): + return node.text + + def visit_literal(self, node:Node, visited_children): + return Literal(node.text) + + def visit_query(self, node:Node, visited_children): + parts = tuple(v for v in flatten(visited_children)) + # assert len(parts) == 2 + return parts + + def visit_match_clause(self, node:Node, visited_children): + return Match(non_null(flatten(visited_children))) + + + def visit_pattern(self, node:Node, visited_children): + return flatten(non_null(visited_children)) # TODO: Try removing non_null + + def visit_assign(self, node: Node, visited_children): + return non_null(visited_children)[0] + + def visit_native(self, node:Node, visited_children): + children = non_null(flatten(visited_children)) + edges = [] + u = children[0] + for constraint in children[1:]: + constraint.may_set_lhs(u) + edges.append(constraint) + return edges + + def visit_constraint(self, node: Node, visited_children): + assert len(visited_children) == 1 + return visited_children[0] + + def visit_has_labelled(self, node: Node, visited_children): + [label, rhs] = non_null(flatten(visited_children)) + if isinstance(rhs, Var): + attr_var = rhs + return [Has(None, attr_var), IsaType(attr_var, label)] + else: + assert isinstance(rhs, Literal) + attr_var = Var.next_internal() + return [Has(None, attr_var), AttributeLabelValue(attr_var, label, rhs)] + + def visit_links(self, node: Node, visited_children): + return non_null(visited_children) + + def visit_role_player(self, node: Node, visited_children): + [role, player] = non_null(flatten(visited_children)) + if isinstance(role, Var): + return [Links(None, player, role)] + else: + assert isinstance(role, Label) + return [Links(None, player, role)] + + def visit_has(self, node: Node, visited_children): + return [Has(None, visited_children[0])] + + def visit_isa(self, node: Node, visited_children): + [label_or_var] = non_null(flatten(visited_children)) + if isinstance(label_or_var, Label): + return [IsaType(None, label_or_var)] + else: + assert isinstance(label_or_var, Var) + return [Isa(None, label_or_var)] + + def visit_comparison(self, node: Node, visited_children): + [lhs, comparator, rhs] = non_null(flatten(visited_children)) + return [Comparison(lhs, rhs, comparator)] + + def visit_comparator(self, node: Node, visited_children): + return [Comparator(node.text)] + + def generic_visit(self, node:Node, visited_children): + """ The generic visit method. """ + return visited_children or None + +if __name__ == "__main__": + input = """ + match + $x isa cow, has name "Spider Georg"; + $y isa cow, has name "Spider Georg"; + $z isa marriage, links (man: $x, woman: $y); + """ + visited = TypeQLVisitor.parse_and_visit(input) + print(visited)