Python SDK

Space and Time Python SDK (Python version >= 3.11)

Note: The recommended approach to storing keys is using an .env file.
For more information, please see:

Installation Instructions

Simply install from the PyPi package index:

pip install spaceandtime

Getting Started

# Initializing the Space and Time usage.
from spaceandtime import SpaceAndTime

sxt = SpaceAndTime()

success, rows = sxt.execute_query(
	'select * from POLYGON.BLOCKS limit 5')
print( rows )

The authentication without arguments will seek out a default .env file and use credentials found there. It also supports passing in a specific filepath.env or simply supplying user_id and private_key.

The generated access_token is valid for 25 minutes and the refresh_token for 30 minutes.

Most database calls will return a tuple of two values: Success as a boolean, and a Result value depending on the specific call.

There are a number of convenience features in the SDK for handling return data sets. By default, data sets are returned as a list-of-dictionaries, however can be easily turned into other formats, such as CSV.

# use triple-quotes to insert more complicated sql:
success, rows = sxt.execute_query("""
	substr(time_stamp,1,7) AS YrMth
	,count(*) AS block_count
	FROM polygon.blocks 
	ORDER BY 1 desc """ )

# print results as CSV
print( sxt.json_to_csv(rows) )

More data transforms will be added over time.

SXTUser Object

All SQL requests are handled by an authenticated user object. The sxt wrapper object in the above examples contains a 'default user' object for simplicity, managing and authenticating as needed. It is however exposed if needed:

print( sxt.user )

You can also manage users directly. This allows you to load and authenticate multiple users at a time, in case your application needs to manage several accounts.

All interaction with the network requires an authenticated user.

The user object owns the authenticated connection to the network, so all requests are submitted by a user object.

# Multiple Users
from spaceandtime import SXTUser

suzy = SXTUser('./users/suzy.env', authenticate=True)

bill = SXTUser()
bill.load()         # defaults to "./.env"

# new user
pat = SXTUser(user_id='pat')
pat.api_url = suzy.api_url # <-- Important! don't lose keys!

There is also some capability to administer your subscription using the SDK. This capability will expand more over time.

# suzy invites pat to her subcription:
if suzy.user_type in ['owner','admin']: 
	joincode = suzy.generate_joincode()
	success, results = pat.join_subscription(joincode)
	print( results )


There are several discovery functions that allow insight to the Space and Time network metadata.

# discovery calls provide network information
success, schemas = sxt.discovery_get_schemas()

print(f'There are {len(schemas)} schemas currently on the network.')

Creating Tables

The SDK abstracts away complexity from making a new table into a Table object. This object contains all needed components to be self-sufficient EXCEPT for an authenticated user object, which again, is required to submit requests to the network.

# Create a table
from spaceandtime import SXTTable, SXTTableAccessType

tableA = SXTTable(name = "SXTTEMP.MyTestTable", 
				new_keypair = True, 
				default_user = sxt.user,
				logger = sxt.logger,
				access_type = SXTTableAccessType.PERMISSSIONED)

tableA.create_ddl = """
	CREATE TABLE {table_name} 
	( MyID         int
	, MyName       varchar
	, MyDate       date
	, Primary Key(MyID) 
	) {with_statement}

# create new biscuits for your table
tableA.add_biscuit('read',  tableA.PERMISSION.SELECT )

tableA.add_biscuit('write', tableA.PERMISSION.SELECT, 
							tableA.PERMISSION.MERGE )

tableA.add_biscuit('admin', tableA.PERMISSION.ALL ) # <-- Important!  Don't lose your keys!

# create with assigned default user
success, results = tableA.create()  

The table.create_ddl and table.with_statement property will substitute {names} to replace with class values. In the example above, the {table_name} will be replace with tableA.table_name and the {with_statement} will be replaced with a valid WITH statement, itself with substitutions for {public_key} and {access_type}.

Note, if the {with_statement} placeholder is absent, the table object will attempt to add dynamically.

When adding biscuits, they can either be added as string tokens, or as SXTBiscuit type objects, or as a list of either.

The function will save all keys, biscuits, and table attributes to a shell-friendly format, such that you could execute the file in shell and load all values to environment variables, for use in other scripting. For example,

Stephen~$ . ./table--SXTTEMP.New_TableName.sql
Stephen~$ echo $TABLE_NAME

This allows table files created in the python SDK to be used with the SxT CLI.

Insert, Deletes, and Selects

There are helper functions to assist quickly adding, removing, and selecting data in the table. Note, these are just helper functions for the specific table object - for more general SQL interface, use the sxt.execute_query() function.

from pprint import pprint # for better viewing of data

# generate some dummy data
cols = ['MyID','MyName','MyDate']
data = [[i, chr(64+i), f'2023-09-0{i}'] for i in list(range(1,10))]

# insert into the table
tableA.insert(columns=cols, data=data)

# select out again, just for fun
success, rows =
pprint( rows )


# one less than last time
success, rows =
pprint( rows )

Creating Views

The SXTView object inherits from the same base class as SXTTable, so the two are very similar. One notable difference is a view's need for a biscuit for each table referenced. To add clarity and remind of this requirement, a view contains a table_biscuit property, although biscuits added this way are simply added to the same internal biscuit collection. Unlike table objects, Views don't need DML PERMISSIONS, like insert or delete.

# create a view 
from spaceandtime import SXTView

viewB = SXTView('SXTTEMP.MyTest_Odds',

viewB.add_biscuit('read', viewB.PERMISSION.SELECT)
viewB.add_biscuit('admin', viewB.PERMISSION.ALL) 
viewB.table_biscuit = tableA.get_biscuit('admin')

viewB.create_ddl = """
	CREATE VIEW {view_name} 
	FROM """ + tableA.table_name + """
	WHERE MyID in (1,3,5,7,9) """ # <-- Important! don't lose keys!

success, results = viewB.create()

We've used the same private key for the table and the view. This is NOT required, but is convenient if you are building a view atop only one table.

Each object comes with a pre-built recommended_filename which acts as the default for save() and load().

print( tableA.recommended_filename )
print( viewB.recommended_filename )
print( suzy.recommended_filename )

Once you're done, it's best practice to clean up.