Usage with EdgeDB
=================
.. image:: ../_static/edgedb.svg
:width: 60%
:align: center
`EdgeDB `__ is an interesting new `graph-relational
database `__
system. It includes a powerful and ergonomic query language `"EdgeQL"
`__, along with client libraries that
integrate well with their respective language ecosystems.
In this example we demonstrate a few ways of integrating EdgeDB's
`Python client library `__
with ``msgspec``.
Setup
-----
This is not intended to be a complete EdgeDB tutorial; for that we recommend
going through the `official EdgeDB quickstart
`__. This example assumes you
already have the EdgeDB CLI and Python library installed.
After cloning the ``msgspec`` repo, navigate to the ``edgedb`` example
directory `here
`__. Then
initialize a new ``edgedb`` project.
.. code-block:: bash
$ edgedb project init --server-instance edgedb-msgspec-example --non-interactive
This will setup a new instance and apply the example schema:
.. literalinclude:: ../../../examples/edgedb/dbschema/default.esdl
We then need to insert some records. This is done with the following EdgeQL
query:
.. literalinclude:: ../../../examples/edgedb/insert_data.edgeql
To run this, execute the following:
.. code-block:: bash
$ edgedb query -f insert_data.edgeql
JSON Encoding Query Results
---------------------------
The EdgeDB Python library returns objects as ``edgedb.Object`` instances
(`docs `__).
Here we query the movie "Dune" that we inserted above, requesting the movie
title and actors' names.
.. code-block:: python
>>> import edgedb
>>> import msgspec
>>> client = edgedb.create_client()
>>> dune = client.query_single(
... """
... SELECT Movie {
... title,
... actors: {
... name
... }
... }
... FILTER .title = 'Dune'
... LIMIT 1
... """
... )
>>> dune
Object{title := 'Dune', actors := [Object{name := 'Timothée Chalamet'}, Object{name := 'Zendaya'}]}
>>> type(dune)
edgedb.Object
These ``edgedb.Object`` instances are duck-type compatible with `dataclasses`,
which means ``msgspec`` already knows how to JSON encode them.
.. code-block:: python
>>> json = msgspec.json.encode(dune)
>>> print(msgspec.json.format(json.decode())) # pretty-print the JSON
{
"id": "b21913c4-3b68-11ee-89b0-2f0b6819503d",
"title": "Dune",
"actors": [
{
"id": "b219195a-3b68-11ee-89b0-5b3794805cc7",
"name": "Timothée Chalamet"
},
{
"id": "b2192058-3b68-11ee-89b0-f7d83b95fb13",
"name": "Zendaya"
}
]
}
Note that if you're immediately JSON encoding the results you may be better
served by using EdgeDB's ``query_json``/``query_single_json`` methods, which
return JSON strings directly (but strip the ``id`` fields).
.. code-block:: python
>>> edgedb_json = client.query_single_json(
... """
... SELECT Movie {
... title,
... actors: {
... name
... }
... }
... FILTER .title = 'Dune'
... LIMIT 1
... """
... )
>>> edgedb_json
'{"title" : "Dune", "actors" : [{"name" : "Timothée Chalamet"},{"name" : "Zendaya"}]}'
If needed, this JSON string may be efficiently composed into a larger JSON
object using `msgspec.Raw`. Here we add some additional outer structure
wrapping the query result:
.. code-block:: python
>>> import datetime
>>> msg = {
... "timestamp": datetime.datetime.now(datetime.timezone.utc),
... "server_version": "3.2",
... "query_result": msgspec.Raw(edgedb_json),
... }
>>> json = msgspec.json.encode(msg)
>>> print(msgspec.json.format(json.decode())) # pretty-print the JSON
{
"timestamp": "2023-08-15T14:37:12.733731Z",
"server_version": "3.2",
"query_result": {
"title": "Dune",
"actors": [
{
"name": "Timothée Chalamet"
},
{
"name": "Zendaya"
}
]
}
}
Supporting Other EdgeDB Types
-----------------------------
Besides ``edgedb.Object``, ``msgspec`` also includes builtin support for JSON
encoding ``edgedb.NamedTuple`` types. There are a few remaining ``edgedb``
types that ``msgspec`` doesn't support out-of-the-box:
- ``edgedb.DateDuration`` (`docs `__)
- ``edgedb.RelativeDuration`` (`docs `__)
JSON encoding support for these may be added through the use of
:doc:`extensions <../extending>`.
.. code-block:: python
>>> def enc_hook(obj):
... if isinstance(obj, (edgedb.DateDuration, edgedb.RelativeDuration)):
... # The str representation of these types are ISO8601 durations,
... return str(obj)
... # Raise a NotImplementedError for unsupported types
... raise NotImplementedError
>>> duration = client.query_single('SELECT "1 year 2 days"')
>>> duration
>>> msgspec.json.encode(duration, enc_hook=enc_hook)
b'"P1Y2D"'
Converting Results to Structs
-----------------------------
If your application contains complex server-side logic, you may wish to convert
the query results into some other application-specific structured type.
``msgspec`` supports automatic conversion to other types `msgspec.convert`.
Here we'll define two `msgspec.Struct` types mirroring our Schema above:
.. code-block:: python
>>> class Person(msgspec.Struct):
... name: str
>>> class Movie(msgspec.Struct):
... title: str
... actors: list[Person]
We can then convert the ``edgedb.Object`` results into our ``Struct`` types
using `msgspec.convert`. Note that the same conversion process would work if
``Person`` or ``Movie`` were defined as `dataclasses` or `attrs` types instead.
.. code-block:: python
>>> msgspec.convert(dune, Movie, from_attributes=True)
Movie(title='Dune', actors=[Person(name='Timothée Chalamet'), Person(name='Zendaya')])
These structs may then be used to implement application logic
(mutating/combining them as needed) before serializing the output to JSON.