README
Surrealist is a Python tool to work with awesome SurrealDB (support for latest version 2.1.3)
It is synchronous and unofficial, so if you need async AND/OR official client, go here
Works and tested on Ubuntu, macOS, Windows 10, can use python 3.8+ (including python 3.13)
Key features:
- only one small dependency (websocket-client), no need to pull a lot of libraries to your project
- fully documented
- well tested (on the latest Ubuntu, macOS and Windows 10)
- fully compatible with the latest version of SurrealDB (2.1.3), including live queries and change feeds
- debug mode to see all that goes in and out if you need (using standard logging)
- iterator to handle big select queries
- QL-builder to explore, generate and use SurrealDB queries (explain, transaction etc.)
- connections pool for use at a high load
- http or websocket transport to use
- always up to date with SurrealDB features and changes
Installation
Via pip:
pip install surrealist
Before you start
Please make sure you install and start SurrealDB, you can read more here
Attention! SurrealDB version 2.0.0 has some breaking changes, so we have to inherit some of them, and you cannot use surrealist version 1.0.0 to work with
Surreal DB version 1.5.3 or earlier. Please consider table to choose a version:
SurrealDB version | 2.0.0+ | 1.5.0+ | 1.4.0+ | 1.3.0+ | 1.2.0+ | 1.1.1+ |
---|
Surrealist version | 1.0.0+ | 0.5.3 | 0.4.2+ | 0.3.1+ | 0.2.10+ | 0.2.3+ |
Python versions | 3.8-3.13 | 3.8-3.12 | 3.8-3.12 | 3.8-3.12 | 3.8-3.12 | 3.8-3.12 |
A good place to start is connect examples here
You can find a lot of examples here
Transports
First of all, you should know that SurrealDB can work with websocket or http "transports", we chose to support both transports here,
but websockets is preferred and default one. Websockets can use live queries and other cool features.
Each transport has functions it cannot use by itself (in a current SurrealDB version)
Http-transport cannot:
- create or kill a live query
- use LET or UNSET methods
Websocket-transport cannot:
- import or export data (you should use http connection or cli tools for that)
- import or export ML files (you should use http connection or cli tools for that)
If you use these methods on transports -CompatibilityError will be raised
Connect to SurrealDB
All you need is url of SurrealDB and sometimes a few more data to connect
Example 1
In this example, we explicitly show all parameters, but remember many of them are optional
from surrealist import Surreal
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"),
use_http=False, timeout=10)
print(surreal.is_ready())
print(surreal.version())
Note: create of a Surreal object does not attempt any connections or other actions, just store parameters for future use
Calls of is_ready(), health() or version() on Surreal objects are for server checks only, these not validate or check your namespace, database or credentials.
Parameters
url - url of SurrealDB server, if you are sure you will use websocket connection - you can use url like ws://127.0.0.1:8000/rpc, but http will work fine too, even for websockets.
So, you can simply use http://127.0.0.1:8000, it will be transform to ws://127.0.0.1:8000/rpc under the hood.
If your url is differed - specify url in ws(s) format
But if you will use ws(s) format, a Surreal object will try to predict http url too; it is important for status and version checks.
For example for wss://127.0.0.1:9000/some/rps predicted http url will be https://127.0.0.1:9000/
namespace - name of the namespace, it is optional, but if you use it, you should specify a database too
database - name of the database, it is optional, but if you use it, you should specify namespace too
credentials - optional, pair(tuple) of username and password for SurrealDB
use_http - optional, False by default, flag of using websockets or http transport, False mean using websocket, specify True if you want to use http transport
timeout - optional, 15 seconds by default, it is time in seconds to wait for responses and messages, time for trying to connect to SurrealDB
Example 2
In this example, we do not use default(optional) parameters
from surrealist import Surreal
surreal = Surreal("http://127.0.0.1:8000")
print(surreal.is_ready())
print(surreal.version())
Context managers and close
You should always close created connections, when you do not need them anymore, the best way to do it is via context manager
Example 3
from surrealist import Surreal
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as ws_connection:
result = ws_connection.select("person")
print(result)
You can do the same by itself:
Example 4
from surrealist import Surreal
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
ws_connection = surreal.connect()
result = ws_connection.select("person")
print(result)
ws_connection.close()
Methods and Query Language
Before you go with surrealist, please check
You can find basic examples here
QL-builder is a simple, convenient way to create queries, validate them and run it against SurealDB.
It is simple, readable and can be the way to learn QL
Example 5
from surrealist import Database
with Database("http://127.0.0.1:8000", 'test', 'test', credentials=("user_db", "user_db")) as db:
table = db.table("person")
print(table.count())
result = table.create("john").set(status="ACTIVE").returns("id").run()
print(result)
print(table.count())
You can find QL examples here
One of the main features of QL-builder is that using dot you can see all statements available on each level,
any modern IDE will show possible statements when you type dot.
Thanks to this, you can study QL and also gain confidence that you are forming a valid query.
for example
db.account.select().limit(50).start_at(50)
analog "SELECT * FROM account LIMIT 50 START 50;"
Pay attention — you can use just table name without using table() method db.person.select()
,
it is readable and shorter, but in that particular case you will not get IDE suggestions.
So, we recommend using table() method db.table("person").select()
it is not much bigger, but still readable,
and you will get help from your IDE
If you cannot form your query with QL, you always can use a raw query via database.raw_query
or connection.query
It is the most efficient way, cause it allows you to do all that is possible if you have permissions.
Iteration on Select
When you expect a lot of data on your select query via QL-builder, you should consider using iterator, it is a simple, lazy and common way to use in python.
Iterator can be used with next method or in for statement
Example 6
from surrealist import Database
with Database("http://127.0.0.1:8000", 'test', 'test', credentials=("user_db", "user_db")) as db:
iterator = db.table("user").select().iter(limit=20)
for result in iterator:
print(result.count())
Results
If the method of connection is not raised, it is always returns SurrealResult object on any response of SurrealDB. It was chosen for simplicity.
Please see examples
Here is standard result:
SurrealResult(id=None, status=OK, result=[{'author': '51ff5faa-d798-4194-93c6-179ce7525a8c', 'id': 'article:⟨51ff5faa-d798-4194-93c6-179ce7525a8c⟩', 'text': '51ff5faa-d798-4194-93c6-179ce7525a8c', 'title': '51ff5faa-d798-4194-93c6-179ce7525a8c'}], query=None, code=None, time=77.25µs, additional_info={})
Here is standard error:
SurrealResult(id=ca3eface-9287-4092-a198-4f91ed27a010, status=ERR, result={'code': -32000, 'message': 'There was a problem with authentication'}, query=None, code=None, time=None, additional_info={})
You can always check for error using is_error() method
if result.is_error():
raise ValueError("Got error")
Besides, a result object has helper methods is_empty, id, ids, get, first, last to work with response of SurrealDB.
You need to read this on SurrealDB recordID: https://docs.surrealdb.com/docs/surrealql/datamodel/ids
Using RecordID
Since version 2.0, SurrealDB never converts strings to record_id, so we have to manage it ourselves.
RecordId object exists for that purpose,
you can see examples here
Although for backward compatibility, you still can use record_id in string format,
it is strongly recommended to use RecordId instead!
Note: RecordId object supports only string or uid/ulid type ids, if you need ranges, object or aray type record_id,
you should create valid query and use connection.query() method
Here we create new person, get record id, wraps in RecordId and use for select:
from surrealist import RecordId
result = ws_connection.create("person", {"name": "John Doe"})
record_id = result.id
print(ws_connection.select("person", record_id=RecordId(record_id)).result)
Simple record_id can have only A-Z, a-z letters and digits 0-9, for any other UTF-8 letters RecordId will generate valid representation with special braces:
from surrealist import get_uuid, RecordId
uuid = get_uuid()
record_id = RecordId(uuid, table="person")
print(record_id.to_valid_string())
create_result = ws_connection.create("person", {"name": "tobie", "age": 30}, record_id)
print(create_result.result)
print(ws_connection.select("person", record_id=record_id).result)
Surreal Datetime
Since version 2.0 SurrealDB never converts values, we send to it, so we need to explicitly use datetime.
For example, if you have a datetime field in your table:
DEFINE FIELD create_time ON person TYPE datetime DEFAULT time::now() PERMISSIONS FULL;
you need to use datetime with prefix to add a new record with that field
from datetime import datetime, timezone
from surrealist import Surreal, to_surreal_datetime_str
surreal = Surreal("http://127.0.0.1:8000", credentials=("root", "root"))
with surreal.connect() as ws_connection:
ws_connection.use("test", "test")
tm = to_surreal_datetime_str(datetime.now(timezone.utc))
result = ws_connection.create("person", {'name': "zzz", 'age': 44, 'active': True, 'create_time': tm})
but if you just use datetime string without d-prefix, you will get an error back
Found '2024-10-22T13:54:40.445833Z' for field `create_time`, with record `person:p8vji2zhvr8z7frhsaex`, but expected a datetime
Logging and Debug mode
As it was said, if you need to debug something, stuck in some problem or just want to know all about data between you and SurrealDB, you can use standard logging.
All library logs will contain "surrealist" prefix. You, as a developer, should choose proper handlers, formats, filters etc.
Surrealist does not use root logger, does not use any handlers and uses only DEBUG, INFO and ERROR level for its events.
For example
Example 7
from logging import basicConfig, INFO, DEBUG
from surrealist import Surreal, LOG_FORMAT
basicConfig(format=LOG_FORMAT, level=INFO)
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as connection:
res = connection.create("article", {"author": "John Doe", "title": "In memoriam", "text": "text"})
if you run it, you get in the console:
2024-05-29 15:59:41,759 : Thread-1 : websocket : INFO : Websocket connected
2024-05-29 15:59:41,762 : MainThread : surrealist.connections.websocket : INFO : Operation: SIGNIN. Data: {'user': 'user_db', 'pass': '******', 'NS': 'test', 'DB': 'test'}
2024-05-29 15:59:41,788 : MainThread : surrealist.connections.websocket : INFO : Got result: SurrealResult(id=c3fcebbc-359f-47d0-822b-a4ad8043f64b, status=OK, result=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3MTY5ODAzODEsIm5iZiI6MTcxNjk4MDM4MSwiZXhwIjoxNzE2OTgzOTgxLCJpc3MiOiJTdXJyZWFsREIiLCJqdGkiOiI0YTQyNWFiNy00NGEyLTQ4OGItYjM4MS05YjUyNDQzYTI5OTQiLCJOUyI6InRlc3QiLCJEQiI6InRlc3QiLCJJRC...
2024-05-29 15:59:41,788 : MainThread : surrealist.connections.websocket : INFO : Connected to ws://127.0.0.1:8000/rpc, params: {'NS': 'test', 'DB': 'test'}, credentials: ('root', '******'), timeout: 15
2024-05-29 15:59:41,788 : MainThread : surrealist.connections.websocket : INFO : Operation: CREATE. Path: article, data: {'author': 'John Doe', 'title': 'In memoriam', 'text': 'text'}
2024-05-29 15:59:41,794 : MainThread : surrealist.connections.websocket : INFO : Got result: SurrealResult(id=b307d67f-b01b-4b71-a319-906fa17b8c72, status=OK, result=[{'author': 'John Doe', 'id': 'article:b44tdiiyb8jw6mcn1tzs', 'text': 'text', 'title': 'In memoriam'}], query=None, code=None, time=None, additional_info={})
2024-05-29 15:59:41,794 : MainThread : surrealist.connection : INFO : The connection was closed
but if in the example above (example 7) you choose "DEBUG" for level, you will see all, including low-level clients' data:
2024-05-29 16:03:58,438 : MainThread : surrealist.clients.websocket : DEBUG : Connecting to ws://127.0.0.1:8000/rpc
2024-05-29 16:03:58,445 : Thread-1 : websocket : INFO : Websocket connected
2024-05-29 16:03:58,458 : MainThread : surrealist.clients.websocket : DEBUG : Connected to ws://127.0.0.1:8000/rpc, timeout is 15 seconds
2024-05-29 16:03:58,458 : MainThread : surrealist.connections.websocket : INFO : Operation: SIGNIN. Data: {'user': 'user_db', 'pass': '******', 'NS': 'test', 'DB': 'test'}
2024-05-29 16:03:58,458 : MainThread : surrealist.clients.websocket : DEBUG : Send data: {"id": "1d5758bb-0879-4d8d-9e14-37c9117669a3", "method": "signin", "params": [{"user": "root", "pass": "******", "NS": "test", "DB": "test"}]}
2024-05-29 16:03:58,484 : Thread-1 : surrealist.clients.websocket : DEBUG : Get message b'{"id":"1d5758bb-0879-4d8d-9e14-37c9117669a3","result":"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3MTY5ODA2MzgsIm5iZiI6MTcxNjk4MDYzOCwiZXhwIjoxNzE2OTg0MjM4LCJpc3MiOiJTdXJyZWFsREIiLCJqdGkiOiIwODhhMWY0My04YzY3LTQ5NjYtYTdjNC02ZGI5NjA0MGNkYmIiLCJOUyI6InRlc3QiLCJEQiI6InRlc3QiLCJJRCI6InJvb3QifQ.1pSbJ'...
2024-05-29 16:03:58,484 : MainThread : surrealist.connections.websocket : INFO : Got result: SurrealResult(id=1d5758bb-0879-4d8d-9e14-37c9117669a3, status=OK, result=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpYXQiOjE3MTY5ODA2MzgsIm5iZiI6MTcxNjk4MDYzOCwiZXhwIjoxNzE2OTg0MjM4LCJpc3MiOiJTdXJyZWFsREIiLCJqdGkiOiIwODhhMWY0My04YzY3LTQ5NjYtYTdjNC02ZGI5NjA0MGNkYmIiLCJOUyI6InRlc3QiLCJEQiI6InRlc3QiLCJJRC...
2024-05-29 16:03:58,484 : MainThread : surrealist.connections.websocket : INFO : Connected to ws://127.0.0.1:8000/rpc, params: {'NS': 'test', 'DB': 'test'}, credentials: ('root', '******'), timeout: 15
2024-05-29 16:03:58,484 : MainThread : surrealist.connections.websocket : INFO : Operation: CREATE. Path: article, data: {'author': 'John Doe', 'title': 'In memoriam', 'text': 'text'}
2024-05-29 16:03:58,484 : MainThread : surrealist.clients.websocket : DEBUG : Send data: {"id": "9bbc90d7-d6dc-4a51-ad97-b765e6b09131", "method": "create", "params": ["article", {"author": "John Doe", "title": "In memoriam", "text": "text"}]}
2024-05-29 16:03:58,490 : Thread-1 : surrealist.clients.websocket : DEBUG : Get message b'{"id":"9bbc90d7-d6dc-4a51-ad97-b765e6b09131","result":[{"author":"John Doe","id":"article:72duj8mef1s97c67dv38","text":"text","title":"In memoriam"}]}'
2024-05-29 16:03:58,491 : MainThread : surrealist.connections.websocket : INFO : Got result: SurrealResult(id=9bbc90d7-d6dc-4a51-ad97-b765e6b09131, status=OK, result=[{'author': 'John Doe', 'id': 'article:72duj8mef1s97c67dv38', 'text': 'text', 'title': 'In memoriam'}], query=None, code=None, time=None, additional_info={})
2024-05-29 16:03:58,491 : MainThread : surrealist.connection : INFO : The connection was closed
2024-05-29 16:03:58,491 : Thread-1 : surrealist.clients.websocket : DEBUG : Close connection to ws://127.0.0.1:8000/rpc
2024-05-29 16:03:58,491 : MainThread : surrealist.clients.websocket : DEBUG : Client is closed connection to ws://127.0.0.1:8000/rpc
Note: passwords and auth information always masked in logs. If you still see it in logs - please, report an issue
Live Query
Live queries let you subscribe to events of desired table when changes happen—you get notification as a simple result or in DIFF format
About live query: https://surrealdb.com/products/lq
Using live select: https://surrealdb.com/docs/surrealdb/surrealql/statements/live
About DIFF (jsonpatch): https://jsonpatch.com
LQ can work only with websockets, you have to provide a callback function to call on any event.
Callback should have signature def any_name(param: Dict) -> None
, so it will be called with python dictionary as only argument
Note 1: if your connection was interrupted or closed, LQ will disappear, and you need to recreate it
Note 2: LQ only produces events which happen after the creation of this LQ
Note 3: LQ is associated with connection, where it was created, if you have two or more connections, LQ will depend only on one,
and will disappear on connection close, even if other connections are still active
Note 4: LQ is stop working after calling REMOVE TABLE for table it listens on. This will be fixed in future SurrealDB versions
Example 8
from time import sleep
from surrealist import Surreal
def call_back(response: dict) -> None:
print(response)
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as connection:
res = connection.live("person", callback=call_back)
live_id = res.result
connection.create("person", {"name": "John", "surname": "Doe"})
sleep(0.5)
in console, you will get:
{'result': {'action': 'CREATE', 'id': 'c2c8952b-b2bc-4d3a-aa68-4609f5818d7c', 'result': {'id': 'person:dik1sm50xr2d5mc7fysi', 'name': 'John', 'surname': 'Doe'}}}
Example 9
from time import sleep
from surrealist import Surreal
def call_back(response: dict) -> None:
print(response)
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as connection:
res = connection.live("person", callback=call_back, return_diff=True)
live_id = res.result
connection.create("person", {"name": "John", "surname": "Doe"})
sleep(0.5)
connection.kill(live_id)
in console, you will get:
{'result': {'action': 'CREATE', 'id': '54a4dd0b-0008-46f4-b4e6-83e466cb4141', 'result': [{'op': 'replace', 'path': '/', 'value': {'id': 'person:fhglyrxkit3j0fnosjqg', 'name': 'John', 'surname': 'Doe'}}]}}
If you do not need LQ anymore, call KILL method, with live_id
You can use a custom live query if you need, it lets you use filters and conditions, as refer here
Example 10
from time import sleep
from surrealist import Surreal
def call_back(response: dict) -> None:
print(response)
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as connection:
res = connection.custom_live("LIVE SELECT * FROM ws_person WHERE age > 18;", callback=call_back)
live_id = res.result
connection.create("ws_person", {"age": 16, "name": "Jane"})
connection.create("ws_person", {"age": 28, "name": "John"})
sleep(0.5)
connection.kill(live_id)
in console, you will get:
{'result': {'action': 'CREATE', 'id': '1f57f2de-354a-43ba-8f39-57000944707c', 'result': {'age': 28, 'id': 'ws_person:awot8zdkg3mqj4wymq8c', 'name': 'John'}}}
Pay attention — there is no info about Jane in events we get from LQ, cause Jane is younger than 18.
Same example with QL-builder:
from time import sleep
from surrealist import Database
def call_back(response: dict) -> None:
print(response)
with Database("http://127.0.0.1:8000", 'test', 'test', credentials=("user_db", "user_db")) as db:
table = db.table("ws_person")
result = table.live(callback=call_back).where("age > 18").run()
live_uid = result.result
table.create().content({"age": 16, "name": "Jane"}).run()
table.create().content({"age": 28, "name": "John"}).run()
sleep(0.1)
result = table.kill(live_uid)
Change Feeds
Changes in the database, such as creating, updating, or deleting, are recorded and played back in another channel.
This channel functions as a stream of messages.
Change Feeds are great for ensuring accurate order and consistent replication of tables or databases. They also provide
immediate updates on any changes made.
Read here: https://surrealdb.com/blog/unlocking-streaming-data-magic-with-surrealdb-live-queries-and-change-feeds
Read here: https://surrealdb.com/products/cf
Under the hood: https://docs.surrealdb.com/docs/surrealql/statements/show
Changes Feed works both for http and websockets!
Let's set up everything:
DEFINE TABLE reading CHANGEFEED 1d;
Note: date and time of your requests should be strict AFTER date and time of creating reading
and it should have d'-prefix
Example 11
from surrealist import Surreal
surreal = Surreal("http://127.0.0.1:8000", namespace="test", database="test", credentials=("user_db", "user_db"))
with surreal.connect() as connection:
res = connection.query('SHOW CHANGES FOR TABLE reading SINCE d"2024-02-06T10:48:08.700483Z" LIMIT 10;')
print(res.result)
connection.query('CREATE reading set story = "long long time ago";')
res = connection.query('SHOW CHANGES FOR TABLE reading SINCE d"2024-02-06T10:48:08.700483Z" LIMIT 10;')
print(res.result)
in the console, you will see
[{'changes': [{'update': {'id': 'reading:w0useg3n9bkne6mei63f', 'story': 'long long time ago'}}], 'versionstamp': 851968}]
Same example via QL-builder:
Example 12
from datetime import datetime, timezone
from surrealist import Database, to_surreal_datetime_str
with Database("http://127.0.0.1:8000", 'test', 'test', credentials=("user_db", "user_db")) as db:
tm = to_surreal_datetime_str(datetime.now(timezone.utc))
res = db.table("reading").show_changes().since(tm).run()
print(res.result)
db.table("reading").create().set(story="long long time ago").run()
res = db.table("reading").show_changes().since(tm).run()
Threads and thread-safety
Remember, SurrealDB is "surreally" fast, so first make sure you need to use multiple threads to work with it, because in many situations
one thread is enough to do the job. Do not fall to premature optimizations.
All objects, including connections, statements, database are thread-safe, so you can use all library features in different threads.
This library was made for using in multithreading environments, remember some rules of thumb:
- if you work with only one server of SurrealDB, you need only one Surreal object
- one Connection/Database object represents exactly one connection (websocket or http) with DB
- it is OK to use connection in different threads, but it can be your bottleneck, as there is only one connection to DB
- with many queries and high load, you should consider using more than one connection, but not too many of them. The number of connections equal to the number of CPU-cores is the best choice
- remember to properly close connections
Connections Pool
And again, please, do not fall to premature optimizations, when working with SurrealDB. But if you consider or expect a high load and/or a lot of
threads, which are use SurrealDB, you can use DatabaseConnectionsPool. It can be used exactly like a Database object, the main difference — you
can specify minimum and maximum connections to use. Under high load, when a lot of data goes in and out in a lot of threads - a pool object can
make job faster and effectively, than one common connection.
On start pool will create minimum number of connections, and on a big load will be creating more and more connections until reach the maximum of them.
By default, the minimum number is equal to CPU cores count for the system.
So any incoming request from your application will use the first non-busy connection it gets from the pool.
Pay attention — new connections can be created, but old connections never be closed until the pool will be closed, so the number of connections can grow,
but never can shrink. It is because of Live Queries, as you remember: LQ always linked to connection, so if connection is closed, LQ stops working.
Example 13
from surrealist import DatabaseConnectionsPool
with DatabaseConnectionsPool("http://127.0.0.1:8000", 'test', 'test', credentials=("user_db", "user_db"), min_connections=10,
max_connections=40) as db:
make_something_with_a_lot_of_threads_or_data(db)
Note: DatabaseConnectionsPool is NOT a singleton, it allows creating as many pools as you like, for example, for different databases or namespaces.
It is your job as a developer to limit number of pools created in your application
Important note: for many and maybe the most cases, one shared connection is enough to do the job. Test it and make sure you really need a connection pool.
Recursion and JSON in Python
SurrealDb has "no limit to the depth of any nested objects or values within", but in Python we have a recursion limit and
standard json library (and str function) use recursion to load and dump objects, so if you will have deep nesting in your objects -
you can get RecursionLimitError.
The best choice here is to rethink your schema and objects, because you probably do
something wrong with such a high level of nesting.
Second choice — increase recursion limit in your system with
import sys
sys.setrecursionlimit(10_000)
Examples
You can find a lot of examples here
Contacts
Mail me at farofwell@gmail.com