edgedb / edgedb-python

The official Python client library for EdgeDB
https://edgedb.com
Apache License 2.0
369 stars 45 forks source link

Implement dataclass for EdgeObject #359

Closed fantix closed 2 years ago

fantix commented 2 years ago

This would allow using the EdgeObject as a dataclass, in particular:

Then we could do something like this with FastAPI as an example:

import fastapi
import edgedb
import uuid
from dataclasses import dataclass

app = fastapi.FastAPI()
client = edgedb.create_client()

@dataclass
class Role:
    id: uuid.UUID
    name: str

    @classmethod
    def __get_validators__(cls):
        return []

@dataclass
class User:
    id: uuid.UUID
    name: str
    role: Role

    @classmethod
    def __get_validators__(cls):
        return []

@app.get("/", response_model=User)
def read_root():
    return client.query("select User { name, role: { name } }")[0]

Schema:

module default {
    type User {
        property name -> str;
        link role -> Role;
    }

    type Role {
        required property name -> str;
    }
}
i0bs commented 2 years ago

Have not tested, but for this part:

return client.query("select User { name, role: { name } }")[0]

Will the first index return us the Role Python dataclass, or the EdgeQL schema of it?

fantix commented 2 years ago

Have not tested, but for this part:

return client.query("select User { name, role: { name } }")[0]

Will the first index return us the Role Python dataclass, or the EdgeQL schema of it?

Oh that's the first object in the result set as we are using query() instead of query_single().

p.s.: Ideally we could do something like:

@app.get("/", response_model=typing.Iterable[User])
def read_root():
    return client.query("select User { name, role: { name } }")

But we're blocked by https://github.com/tiangolo/fastapi/pull/3913 - so using a regular list is the current alternative.

fantix commented 2 years ago

Furthermore, we will be able to separate this into 2 separate files:

user_role.edgeql:

select User { name, role: { name } }

user_role_edgeql.py:

import edgedb
import typing
import uuid
from dataclasses import dataclass

@dataclass
class Role:
    id: uuid.UUID
    name: str

    @classmethod
    def __get_validators__(cls):
        return []

@dataclass
class User:
    id: uuid.UUID
    name: str
    role: Role

    @classmethod
    def __get_validators__(cls):
        return []

def query(client: edgedb.Client) -> typing.Sequence[User]:
    return client.query("select User { name, role: { name } }")

So that we can generate user_role_edgeql.py from user_role.edgeql with a console command.

i0bs commented 2 years ago

Oh that's the first object in the result set as we are using query() instead of query_single().

p.s.: Ideally we could do something like:

@app.get("/", response_model=typing.Iterable[User])
def read_root():
    return client.query("select User { name, role: { name } }")

But we're blocked by tiangolo/fastapi#3913 - so using a regular list is the current alternative.

That would be preferred, or you could subsequently use typing.Mapping to make a relationship between the dataclass and then client-defined typings. Would that also work?

fantix commented 2 years ago

subsequently use typing.Mapping to make a relationship between the dataclass and then client-defined typings. Would that also work?

An example please? Sorry I'm not sure I'm 100% following.

i0bs commented 2 years ago

An example please? Sorry I'm not sure I'm 100% following.

typing.Mapping works like a container, you essentially would have an object that returns a value type based on the key type:

class CustomMapping(typing.Generic[KV, KT]):
    def __getitem__(self, key: KT) -> VT:
        ...

This would only be useful if you have custom-defined typings in the client library. For example, you could use type variables to represent types from EdgeQL.

This could be used as:

@app.get("/", response_model=CustomMapping[User, Role])
def read_root():
    return client.query("select User { name, role: { name } }")