Skip to content
RKGMS
SDK GUIDES
Python

RelationalAI SDK for Python

This guide presents the API for the RelationalAI SDK for Python.

python logo

This guide presents the main features of the RelationalAI SDK for Python, used to interact with the Relational Knowledge Graph Management System (RKGMS).

The rai-sdk-python package is open source and is available in this Github repository:


RelationalAI/rai-sdk-python

It includes self-contained examples of the main API functionality. We welcome contributions and pull requests.

Note: This guide applies to rai-sdk-python, the latest iteration of the Python SDK. The relationalai-sdk package is deprecated.

The last section describes how the different Console notebook cell types can be implemented with this Python SDK API.

Requirements

Check the rai-sdk-python repository for the latest version requirements to interact with the RKGMS using the RelationalAI SDK for Python.

Installation

The RelationalAI SDK for Python is a standalone client library and can be installed using the poetry or pip Python package managers as shown below:

# using the poetry package manager
poetry add rai-sdk
# using the pip package manager
pip install rai-sdk

Configuration

To connect to the RAI Server using the Python SDK, you need a configuration file. See the SDK Configuration guide for more details. If you get “local issuer certificate” errors you may need to install local certificates for your Python.

The Python API config.read() function takes the configuration file and the profile name as optional arguments:

from railib import config
cfg = config.read("~/.rai/config", profile = "default")

To load a different configuration, you can simply replace "default" with a different profile name.

Creating a Context

Most API operations use a context object, constructed with railib.api.Context. To use the default profile in your ~/.rai/config file, use:

from railib import api, config
cfg = config.read()
# to specify a non-default profile instead:
# cfg = config.read(profile='myprofile')
ctx = api.Context(**cfg)

As an alternative to loading a configuration and using **cfg, you can also specify the different fields directly using keyword arguments in api.Context. See help(api.Context) for details.

You can test your configuration and context by running:

api.list_databases(ctx)

This should return a list with database info, assuming your keys have the corresponding permissions (see Listing Databases below).

The remaining code examples in this document assume that you have a valid context in the ctx Python variable, and the following imports:

from railib import api, config, show
import json
from urllib.request import HTTPError

Managing Users

A client with the right permissions can create, disable, and list the users under the account.

Creating a User

create_user(ctx, userid, roles)

Here, userid is a string, identifying the user, and roles is a list of railib.api.Role (default None, which assigns the user role).

Current valid roles are created with api.Role(<rolestring>). For a list of roles

>>> [r.value for r in api.Role]
['user', 'admin']

Disabling a User

disable_user(ctx, user)

Listing Users

rsp = api.list_users(ctx)
print(json.dumps(rsp, indent=2))

Retrieving User Details

import json
rsp = api.get_user(ctx, user)
print(json.dumps(rsp, indent=2))

Here, user is a string ID, e.g. "auth0|XXXXXXXXXXXXXXXXXX".

Managing Oauth Clients

Oauth clients can be managed with the following functions, provided you have the corresponding permissions:

create_oauth_client(ctx, name, permissions)

name is a string identifying the client. permissions is a list of railib.api.Permission (default None, which assigns no permissions).

For a list of permissions, see:

>>> [p.value for p in api.Permission]
['create:compute', 'delete:compute', 'list:compute', 'read:compute',
 'list:database', 'update:database', 'delete:database',
 'run:transaction', 'read:credits_usage',
 'create:oauth_client', 'read:oauth_client',
 'list:oauth_client', 'update:oauth_client', 'delete:oauth_client',
 'rotate:oauth_client_secret',
 'create:user', 'list:user', 'read:user', 'update:user',
 'list:role', 'read:role',
 'list:permission', 'create:accesskey', 'list:accesskey']

Get a list of oauth clients:

list_oauth_clients(ctx)

Get details for a specific oauth client, identified by the string id:

get_oauth_client(ctx, id)

Delete the oauth client identified by the string id:

delete_oauth_client(ctx, id)

Managing Engines

To query and update RAICloud databases, you will need a running engine. The following API calls create and manage them:

Creating an Engine

You can create a new engine as follows:

from urllib.request import HTTPError
engine_name="python_sdk_engine"
size=api.EngineSize.XS
try:
    response = api.create_engine(ctx, engine_name, size)
    print(json.dumps(response, indent=2))
except HTTPError as e:
    show.http_error(e)

API requests return a JSON value, represented in Python as a Dict, which can be converted to a string with json.dumps and then printed, as above. Here is a sample result for api.create_engine:

{'engine': {
    'account_name': '#########',
    'created_by': '#########',
    'id': '#########',
    'name': 'python_sdk_engine',
    'region': '#########',
    'requested_on': '2021-08-17T15:32:47.371Z',
    'size': 'XS',
    'state': 'REQUESTED'
    }
}

Valid sizes are given by the api.EngineSize enum:

  • XS (extra small)
  • S (small)
  • M (medium)
  • L (large)
  • XL (extra large)

Note: It may take some time before your engine is in the “PROVISIONED” state, where it is ready for queries. It will be in the “PROVISIONING” state before that.

To list all the engines that are currently provisioned, we can use api.list_engines:

from railib import api
import json
print(json.dumps(api.list_engines(ctx, "PROVISIONED"), indent=2))

If there is an error with the request, an urllib.request.HTTPError exception will be thrown.


Most of the API examples below assume we have a running engine (in “PROVISIONED” state) in the engine variable, and a test database in database:

# replace by your values for testing:
database = "mydatabase"
engine = "MYENGINE"

Deleting an Engine

An engine can be deleted with:

from railib import api, show
from urllib.request import HTTPError
try:
    rsp = api.delete_engine(ctx, engine)
except HTTPError as e:
    show.http_error(e)

If successful, this will return:

{'status':
    {'name': XXXXXX
    'state': 'DELETING',
    'message': 'engine XXXX deleted successfully'}
}

Note that since RAICloud decouples computation from storage, deleting an engine does not delete any cloud databases. See Managing Engines for more details.

Getting Info for an Engine

The details for a specific compute engine can be retrieved with api.get_engine:

api.get_engine(ctx, engine)

This will return an empty list if the engine does not exist.

Managing Databases

Creating a Database

You can create a database with api.create_database, as follows:

from railib import api, config, show
from urllib.request import HTTPError
import json
# Create a database
database = "mydatabase" # adjust as needed
engine = "MYENGINE"
try:
    rsp = api.create_database(ctx, database, engine)
    print(json.dumps(rsp, indent=2))
except HTTPError as e:
    show.http_error(e)

The result from a successful create_database call will look like this:

{
  "output": [],
  "version": 2,
  "problems": [],
  "actions": [],
  "debug_level": 0,
  "aborted": false,
  "type": "TransactionResult"
}

A failed call will have a non-empty problems list.

Cloning a Database

The call api.create_database can be used to clone a database by specifying a source argument:

from railib import api, config
import json
from urllib.request import HTTPError
# Clone a database
try:
    rsp = api.create_database(ctx, "MYDBCLONE", engine,
                          source=database)
    print(json.dumps(rsp, indent=2))
except HTTPError as e:
    show.http_error(e)

With this API call, you can clone a database from database to "MYDBCLONE", creating an identical copy. Any subsequent changes to either database will not affect the other. Cloning a database only succeeds if the source database already exists.

Retrieving Database Details

rsp = api.get_database(ctx, databasename)
print(json.dumps(rsp, indent=2))

The response is a JSON object. If the database does not exist, an [] will be returned.

Note that this call does not require a running engine.

Listing Databases

Using api.list_databases will list the databases available to the account:

rsp = api.list_databases(ctx)
# rsp = api.list_databases(ctx, state)
print(json.dumps(rsp, indent=2))

The optional variable state (default: None) can be used to filter databases by state. For example, “CREATED”, “CREATING”, or “CREATION_FAILED”.

Deleting Databases

A database can be deleted with api.delete_database, if the config has the right permissions.

The database is identified by its name (as used in create_database).

from railib import api, show
from urllib.request import HTTPError
try:
    rsp = api.delete_database(ctx, database_name)
except HTTPError as e:
    show.http_error(e)

If successful, the response will be of the form

{'name': 'XXXXXXX', 'message': 'deleted successfully'}

Deleting a database cannot be undone.

Rel Model Sources

Rel model sources are collections of Rel code that can be added, updated, or deleted from a particular database. Since they update a database, a running engine is required to perform operations on sources.

Installing Rel Model Sources

The api.install_model function installs a Rel model source in a given database. The last argument is a Python Dictionary, mapping names to models, so that more than one named model can be installed.

For example, to add a Rel model source code file to a database:

source_string = """
def countries = {"United States of America"; "Germany"; "Japan"; "Greece"}
def oceans = {"Arctic"; "Atlantic"; "Indian"; "Pacific"; "Southern"}
"""

api.install_model(ctx, database, engine, {"mysource" : source_string})

If the database already contains an installed source with the same given name, then it is replaced by the new source.

If you need to install from a file, read it into a string first. For example:

from os import path
msources = {}
with open(fname) as fp:
    msources[path.basename(fname)] = fp.read()  # source name => source
api.install_model(ctx, database, engine, msources)

Deleting a Rel Model Source

You can delete a source from a database using the following code:

rsp = api.delete_model(ctx, database, engine, "sourcename")

Listing Installed Rel Model Sources

You can list the sources in a database using the following code:

api.list_models(ctx, database, engine)

This returns a JSON array of names.

To see the contents of a named source, use:

api.get_model(ctx, database, engine, sourcename)

where sourcename is a string.

Querying a Database

A query is a high-level API call to run a single query/transaction against the database. It specifies a Rel program (which can be empty), and a set of output relations to be returned.

api.query(ctx: railib.api.Context, database: str, engine: str, command: str,
    inputs: dict = None,
    readonly: bool = True) -> dict

For example:

>>> rsp = api.query(ctx, database, engine, "def output[x in {1;2;3}] = x * 2")
>>> show.results(rsp)
// output (Int64)
1, 2;
2, 4;
3, 6

By convention, only the output relation is returned (as in Console notebooks), and readonly is True.

Queries meant to update base relations (with insert and delete) must use readonly=false.

For example, here is an API call to load some CSV-like data and store it in the base relation my_base_relation:

data = """
name,lastname,id
John,Smith,1
Peter,Jones,2
"""
api.query(ctx, database, engine, """
def config:schema:name="string"
def config:schema:lastname="string"
def config:schema:id="int"
def config:syntax:header_row=1
def config:data = mydata

def delete[:my_base_relation] = my_base_relation
def insert[:my_base_relation] = load_csv[config]
""", inputs = {"mydata":data}, readonly=False)

The query size is limited to 64MB. An HTTPError exception will be thrown if the request exceeds this API limit.

Getting Multiple Relations Back

As in the notebooks, if you want to return multiple relations, you can define sub-relations of output. For example:

>>> resp = api.query(ctx, database, engine,
                     "def a = 1;2 def b = 3;4 def output:one = a def output:two = b")
>>> show.results(resp)
// output (:two*Int64)
3;
4

// output (:one*Int64)
1;
2

Result Structure

The response is a Python dictionary with one or more of the following keys:

fieldmeaning
outputA list of Python Dict’s, each one corresponding to an output relation — in this case, only one, output. (Future releases of the SDK may support multiple relations here.)
versionDeprecated
problemsInformation about any existing problems in the DB (which are not necessarily caused by the query)
actions[can ignore]
abortedIndicator whether an abort resulted — for example, if an Integrity Constraint was violated.
type[can ignore]

Specifying Inputs

The api.query method takes an optional inputs dictionary that can be used to map relation names to string constants for the duration of the query, analogous to the file upload feature of Rel Console notebooks. Thus, for example,

api.query(ctx, database, engine, "def output = foo", inputs = {"foo" : "asdf"})

will return the string "asdf" back.

Functions that transform a file and write the results to a base relation can be easily written in this way. The calls api.load_csv and api.load_json are special cases of this.

Printing Responses

The railib.show function can be used to print API responses. For example:

>>> response = api.query(ctx, database, engine, "def output = 'a';'b';'c'")
>>> show.results(response)
// output (Char)
"a";
"b";
"c"

The function show.problems(response) prints problems.

See the examples folder for examples on how to handle HTTP exceptions.

Loading Data: load_csv and load_json

As a convenience, the Python API includes load_csv and load_json Python functions. These are not strictly necessary, since the load utilities in Rel itself can be used in a non-read-only api.query that uses the inputs option.

The call api.load_csv loads data and inserts the result into the base relation named by the relation argument.

load_csv(ctx: railib.api.Context, database: str, engine: str, relation: str, data, syntax: dict = {})
    # `syntax`:
    #   * header: a map from col number to name (base 1)
    #   * header_row: row number of header, 0 means no header (default: 1)
    #   * delim: default: ,
    #   * quotechar: default: "
    #   * escapechar: default: \
    #
    # Schema: a map from col name to rel type name, eg:
    #   {'a': "int", 'b': "string"}

Similarly, load_json loads the data string as JSON, and inserts it into the base relation named by the relation argument:

load_json(ctx: railib.api.Context, database: str, engine: str, relation: str, data) -> dict

Example:

api.load_json(ctx, database, engine, "myjson",  """{"a" : "b"}""")

Note: in both cases, the relation base relation is not cleared, allowing for multi-part, incremental loads. To clear it, issue a non-read-only query of the form:

def delete[:relation] = relation

Listing Base Relations

api.list_edbs(ctx, database, engine)

will list the base relations in the given database. The result is a JSON list of objects.

Notebook Cell Types

As explained in Working with RAI Notebooks there are four notebook cell types: Query, Install, Update, and Markdown. In this section, we show how the three cell types (Query, Install, and Update) map to Python SDK API calls.

Query Cells

The Query cell in a Rel Console notebook corresponds to the relationalai.Connection.query() Python function call as shown below:

>>> rsp = api.query(ctx, database, engine, "def output = {(1,); (2,); (3,)}")
>>> show.results(rsp)
// output (Int64)
1;
2;
3
show.problems(rsp)
<None>

Note that no rules/definitions are persisted in this case.

Install Cells

If derived relation definitions need to be persisted, we use the install_model API call. That is, the Install cell in a Rel Console notebook corresponds to the api.install_model Python function call, as shown below:

>>> rsp = api.install_model(ctx, database, engine,
    {"arity1_k_def" : "def k = {2;3;5}"} )
>>> show.problems(rsp)
<None>

Let’s check that the above relation was persisted using the query() API call:

>>> rsp = api.query(ctx, database, engine, "def output = k")
>>> show.results(rsp)
// output (Int64)
2;
3;
5

Update Cells

The Update cell in a Rel Console notebook corresponds to an api.query Python function call with the flag readonly=False. For example:

rsp = api.query(ctx, database, engine, """def insert:employee = {(1, \"Han Solo\");
         (2, \"Bart Simpson\")}""", readonly=False)

Now let’s modify the employee base relation:

rsp = api.query(ctx, database, engine, """def insert:employee = {(3, \"King\");
          (4, \"Queen\")}""", readonly=False)

Now let’s query the modified employee base relation:

>>> rsp = api.query(ctx, database, engine, "def output=employee")
>>> show.results(rsp)
// output (Int64*String)
1, "Han Solo";
2, "Bart Simpson";
3, "King";
4, "Queen"

To update base relations, always use the readonly=False option in the query() call.