Skip to content
RKGS
SDK GUIDES
Python

RelationalAI SDK for Python

This guide presents the main features of the RelationalAI SDK for Python, which can be used to interact with RelationalAI’s Relational Knowledge Graph System (RKGS).

python logo

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


RelationalAI/rai-sdk-python

It includes self-contained examples (opens in a new tab) of the main API functionality. Contributions and pull requests are welcome.

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

See also Python SDK Through Visual Studio Code if you are using a Microsoft VS Code environment to work with the RelationalAI SDK for Python.

Requirements

You can check the rai-sdk-python (opens in a new tab) repository for the latest version requirements to interact with the RKGS using the RelationalAI SDK for Python.

Installation

The RelationalAI SDK for Python is a stand-alone client library and can be installed using the pip Python package manager as shown below:

# using the pip package manager
pip install rai-sdk

Additional ways of installing the library can be found in the rai-sdk-python (opens in a new tab) repository.

Configuration

The RelationalAI SDK for Python can access your RAI Server credentials using a configuration file. See SDK Configuration for more details. If you get “local issuer certificate” errors, you may need to install local certificates for your Python (opens in a new tab).

The Python API config.read() function takes the configuration file and the profile name as optional arguments. For instance, you can write the following configuration in a .py file or directly from the Python command-line interface:

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

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

Creating a Context

Most API operations use a context object, constructed with railib.api.Context. To apply the default profile in your ~/.rai/config file, you can 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:

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

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

Another way of testing your setup is by running one of the examples from the repository. You can do that directly from a terminal:

$ cd examples
$ python3 ./list_databases.py

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

Moreover, all of them can be run from the Python command-line interface or a .py file.

Managing Users

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

Creating a User

api.create_user(ctx, userid, roles)

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

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

rsp = [r.value for r in api.Role]
print(json.dumps(rsp, indent=2))
 
["user", "admin"]

Deleting a User

You can delete a user through:

api.delete_user(ctx, id)

In this case, id is a string reflecting a given user’s ID.

Disabling and Enabling a User

You can disable a user through:

api.disable_user(ctx, id)

Again, id is a string representing a given user’s ID. You can reenable the user as follows:

api.enable_user(ctx, id)

Listing Users

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

Retrieving User Details

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

Here, user is a string ID, for example, "auth0|XXXXXXXXXXXXXXXXXX".

Managing OAuth Clients

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

api.create_oauth_client(ctx, name, permissions)

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

For a list of permissions, see:

rsp = [p.value for p in api.Permission]
print(json.dumps(rsp, indent=2))
 
[
  "create:engine", "delete:engine", "list:engine", "read:engine",
  "list:database", "update:database", "delete:database",
  "run:transaction", "read: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"
]

This is how to get a list of OAuth clients:

api.list_oauth_clients(ctx)

This is how to get details for a specific OAuth client, identified by the string id:

api.get_oauth_client(ctx, id)

Here’s how to delete the OAuth client identified by the string id:

api.delete_oauth_client(ctx, id)

Each OAuth client has its own set of permissions, which determine the operations it can execute. Depending on the permissions, some operations, such as listing other users and creating or deleting engines, may fail. See the RAI Console Managing Users guide for more details.

Managing Engines

To query and update RelationalAI 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:

engine_name="python_sdk_engine"
size=api.EngineSize.XS
try:
    rsp = api.create_engine(ctx, engine_name, size)
    print(json.dumps(rsp, 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:

{
  "compute": {
    "account_name": "#########",
    "id": "#########",
    "name": "python_sdk_engine",
    "size": "XS",
    "region": "#########",
    "state": "REQUESTED",
    "created_by": "#########",
    "requested_on": "2022-08-10T11:22:30.699Z"
  }
}
💡

In the example above, the term compute refers to the engine.

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, you can use api.list_engines:

rsp = api.list_engines(ctx, "PROVISIONED")
print(json.dumps(rsp, indent=2))

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


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

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

Deleting an Engine

Here’s how to delete an engine:

try:
    rsp = api.delete_engine(ctx, engine)
except HTTPError as e:
    show.http_error(e)

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

Getting Info for an Engine

You can retrieve the details for a specific engine 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:

database = "mydatabase"
try:
    rsp = api.create_database(ctx, database)
    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:

{
  "database": {
    "account_name": "#########",
    "name": "mydatabase",
    "id": "#########",
    "state": "CREATED",
    "region": "#########",
    "created_on": "2022-08-10T11:50:46.078Z",
    "created_by": "#########"
  }
}

A failed call will return a status message such as:

{
  "status": "Conflict",
  "message": "database already exists"
}

Cloning a Database

You can use the api.create_database call to clone a database by specifying a source argument:

try:
    rsp = api.create_database(ctx, "MYDBCLONE", 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 fails if the source database does not exist.

You cannot clone from a database until an engine has executed at least one transaction on that database.

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, [] 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

You can delete a database with api.delete_database, if the config has the right permissions.

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

try:
    rsp = api.delete_database(ctx, database_name)
    print(json.dumps(rsp, indent=2))
except HTTPError as e:
    show.http_error(e)

If successful, the response will be of the form:

{
  "name": "#########",
  "message": "deleted successfully"
}

Deleting a database cannot be undone.

Rel Models

Rel models are collections of Rel code that can be added, updated, or deleted from a dedicated database. A running engine — and a database — is required to perform operations on models.

Loading a Model

The api.install_model function loads a Rel model 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, this is how to add a Rel model code file to a database:

model_string = """ def countries = {"United States of America"; "Germany"; "Japan"; "Greece"}
def oceans = {"Arctic"; "Atlantic"; "Indian"; "Pacific"; "Southern"} """
 
api.install_model(ctx, database, engine, {"mymodel" : model_string})

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

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

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

The argument fname is a string.

Note that you can also load models in a specific folder by adding the directory in the API function call:

api.install_model(ctx, database, engine, {"mymodels/mymodel" : model_string})

Loading Multiple Models

You can also provide a Python dictionary with a collection of models, together with their names.

Here is an example that loads multiple models at once:

model_string = """ def countries = {"United States of America"; "Germany"; "Japan"; "Greece"}"""
model_string2 = """ def oceans = {"Arctic"; "Atlantic"; "Indian"; "Pacific"; "Southern"}"""
 
api.install_model(ctx, database, engine, {"mymodel" : model_string, "mymodel2" : model_string2})

Deleting Models

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

api.delete_model(ctx, database, engine, model_name)

Note that model_name is a string vector containing the names of the model or models to be deleted.

Listing Installed Models

You can list the models 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 model, you can use:

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

Note that the argument modelname is a string.

Querying a Database

The high-level API call for executing queries against the database is exec. The function call blocks until the transaction is completed or there are several timeouts indicating that the system may be inaccessible. It specifies a Rel source, which can be empty, and a set of input relations:

api.exec(
    ctx: api.Context,
    database: str,
    engine: str,
    command: str,
    inputs: dict = None,
    readonly: bool = True
)

Here is an example of a read query using exec:

rsp = api.exec(ctx, database, engine,  "def output = {1; 2; 3}")
print(rsp)
 
[
  {
    "id": "#########",
    "response_format_version": "2.0.3",
    "state": "COMPLETED"
  },
  [
    {
      "relationId": "/:output/Int64",
      "types": [
        ":output",
        "Int64"
      ]
    }
  ],
  [],
  {
    "relation_count": 1
  },
  {
    "v1": [
      1,
      2,
      3
    ]
  }
]

By default, readonly is true.

Write queries, which update base relations through the control relations insert and delete, must use readonly=false.

Here is an API call to load some CSV data and store them in the base relation my_base_relation:

data = """
name,lastname,id
John,Smith,1
Peter,Jones,2
"""
api.exec(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 RelationalAI SDK for Python also supports asynchronous transactions, through exec_async. In summary, when you issue a query to the database, the return output contains a transaction ID that can subsequently be used to retrieve the actual query results.

exec_async is defined as exec, but in this case the running processes are not blocked:

rsp_async= api.exec_async(
    ctx,
    database, engine,
    "def output = {1;2;3}"
)

Then, you can poll the transaction until it has completed or aborted. Finally, you can fetch the results:

if rsp_async.transaction['state'] == "COMPLETED":
    show.results(rsp_async)

Similarly, you can get the results, metadata, and problems for a given transaction ID using the following functions:

results = api.get_transaction_results(ctx, rsp_async.transaction['id'])
metadata = api.get_transaction_metadata(ctx, rsp_async.transaction['id'])
problems = api.get_transaction_problems(ctx, rsp_async.transaction['id'])

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

Getting Multiple Relations Back

In order to return multiple relations, you can define subrelations of output. For example:

rsp = api.exec(ctx, database, engine,
    "def a = 1;2 def b = 3;4 def output:one = a def output:two = b")
show.results(rsp)
 
{
    "v1": [
      3,
      4
    ]
},
{
    "v1": [
      1,
      2
    ]
}

Result Structure

The response is a Python dictionary with the following keys:

FieldMeaning
metadataMetadata information about the results key.
problemsInformation about any existing problems in the database — which are not necessarily caused by the query.
resultsQuery output information.
transactionInformation about transaction status, including identifier.

The results key is a vector with the following fields:

FieldMeaning
relationIDThis is a key for the relation, for example, "v1". It refers to the column name in the Arrow table that contains the data, where "v" stands for variable, since a relation’s tuples contain several variables.
tableThis contains the results of the query in a JSON-array format.

Each query is a complete transaction, executed in the context of the provided database.

The metadata key is a JSON string with the following fields:

FieldMeaning
relationIDThis is a key for the relation, for example, "/:output/:two/Int64". This key describes the keys of the relation together with the type of data.
typesThis is a JSON-array that contains the key names of the relation and their data type.

Finally, the problems key is also a JSON string with the following fields:

FieldMeaning
error_codeThe type of error that happened, for example, "PARSE_ERROR".
is_errorWhether an error occurred or there was some other problem.
is_exceptionWhether an exception occurred or there was some other problem.
messageA short description of the problem.
pathA file path for the cases when such a path was used.
reportA long description of the problem.
typeThe type of problem, for example, "ClientProblem".

Specifying Inputs

The exec API call takes an optional inputs dictionary that can be used to map relation names to string constants for the duration of the query. For example:

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

This returns the string "asdf" back.

Functions that transform a file and write the results to a base relation can be written in this way. The calls api.load_csv and api.load_json are special cases of this. See, for example, the sample code using load_csv in Querying a Database.

Printing Responses

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

rsp = api.exec(ctx, database, engine, "def output = {1;2;3}")
show.results(rsp)
show.problems(rsp)

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 exec call that uses the inputs option. See the Specifying Inputs section for further details.

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

api.load_csv(ctx: 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:

api.load_json(ctx: api.Context, database: str, engine: str, relation: str, data)

Example:

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

Note: In both cases, the relation base relation is not cleared, allowing for multipart, incremental loads. To clear it, you can do:

def delete[:relation] = relation

Listing Base Relations

api.list_edbs(ctx, database, engine)

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

Transaction Cancellation

You can cancel an ongoing transaction by calling the following function:

api.cancel_transaction(ctx, id)

The argument id is a string that represents the transaction ID; for instance, rsp[0]["id"] from a previous exec API call.

Was this doc helpful?