strawberry-graphql / strawberry-sqlalchemy Goto Github PK
View Code? Open in Web Editor NEWA SQLAlchemy Integration for strawberry-graphql
License: MIT License
A SQLAlchemy Integration for strawberry-graphql
License: MIT License
python 3.11
sqlalchemy 2.0.23
strawberry-graphql 0.216.0
strawberry-sqlalchemy-mapper 0.4.0
Trying to use this mapper and was playing around with some basics but can't get relationships to work. It doesn't seem like i do something wrong, this is literally the example from readme:
models.py
from sqlalchemy import Column, UUID, String, ForeignKey
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import relationship
Base = declarative_base()
class Employee(Base):
__tablename__ = "employee"
id = Column(UUID, primary_key=True)
name = Column(String, nullable=False)
password_hash = Column(String, nullable=False)
department_id = Column(UUID, ForeignKey("department.id"))
department = relationship("Department", back_populates="employees")
class Department(Base):
__tablename__ = "department"
id = Column(UUID, primary_key=True)
name = Column(String, nullable=False)
employees = relationship("Employee", back_populates="department")
app.py
from fastapi import FastAPI
from strawberry.fastapi import GraphQLRouter
import strawberry, models
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
@strawberry_sqlalchemy_mapper.type(models.Employee)
class Employee:
__exclude__ = ["password_hash"]
@strawberry_sqlalchemy_mapper.type(models.Department)
class Department:
pass
@strawberry.type
class Query:
@strawberry.field
def departments(self) -> Department:
return Department(name="Test", employees=[Employee(name="1"), Employee(name="2")])
strawberry_sqlalchemy_mapper.finalize()
schema = strawberry.Schema(
query=Query,
)
app = FastAPI()
graphql_app = GraphQLRouter(schema)
app.include_router(graphql_app, prefix='/graphql')
main.py
import uvicorn
from app import app
if __name__ == "__main__":
uvicorn.run(app, port=5000, log_level="info")
Got this error:
Traceback (most recent call last):
File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 808, in fields
fields = resolve_thunk(self._fields)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
return thunk() if callable(thunk) else thunk
^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 514, in <lambda>
fields=lambda: self.get_graphql_fields(object_type),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 373, in get_graphql_fields
return _get_thunk_mapping(
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 133, in _get_thunk_mapping
thunk_mapping[name_converter(field)] = field_converter(
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 314, in from_field
self.from_maybe_optional(
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 768, in from_maybe_optional
return GraphQLNonNull(self.from_type(type_))
^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 801, in from_type
raise TypeError(f"Unexpected type '{type_}'")
TypeError: Unexpected type 'ForwardRef('Employee')'
The above exception was the direct cause of the following exception:
Traceback (most recent call last):
File "/app/app.py", line 28, in <module>
schema = strawberry.Schema(
^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema.py", line 141, in __init__
self._schema = GraphQLSchema(
^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 224, in __init__
collect_referenced_types(query)
File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
collect_referenced_types(field.type)
File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
collect_referenced_types(field.type)
File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 433, in collect_referenced_types
collect_referenced_types(field.type)
File "/usr/local/lib/python3.11/site-packages/graphql/type/schema.py", line 432, in collect_referenced_types
for field in named_type.fields.values():
^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/functools.py", line 1001, in __get__
val = self.func(instance)
^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 811, in fields
raise cls(f"{self.name} fields cannot be resolved. {error}") from error
TypeError: EmployeeEdge fields cannot be resolved. Unexpected type 'ForwardRef('Employee')'
Hi,
is there a way to pass selected fields to the Query so that instead select * from a huge document/table I get only fields I really need?
When combining the @strawberry.input and @strawberry_sqlalchemy_mapper.type decorators i was able to declare a valid strawberry input type.
However, i came into a problem trying to define which fields (generated by strawberry_sqlalchemy_mapper.type from the model) are optional (without overriding their previous strawberry_sqlalchemy_mapper declaration).
Could be useful to make a strawberry_sqlalchemy_mapper.input decorator that implements this (perhaps by checking 'SQLAlchemy.Column.nullable' property, or simply by listing them) and wraps both @strawberry.input and @strawberry_sqlalchemy_mapper.type.
EDIT
After examining the source code i see the mapper does take optional into account, it just does not reflect that when working with the @strawberry.input decorator (raises __init__() missing # required keyword-only argument(s)
)
possible use:
# models.py
from sqlalchemy import Column, Integer, String
from sqlalchemy.ext.declarative import declarative_base
Base = declarative_base()
class User(Base):
__tablename__ = "user"
username= Column(String, primary_key=True)
password_hash = Column(String, nullable=False)
first_name = Column(String, nullable=False)
last_name = Column(String, nullable=True)
# elsewhere
# ...
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
@strawberry_sqlalchemy_mapper.input(models.User)
class UserInput:
__exclude__ = ["password_hash"]
__optional__ = ["last_name"] # or read nullable attribute
password : str
I try load user and comment relationship in video model using async session like
`
query listVideo{
listVideo{
url,
description,
title,
createdAt,
updatedAt,
user{
username
id
email
}
comments{
edges{
node{
text
}
}
}
}
}
`
but i got error
RuntimeError: readexactly() called while another coroutine is already waiting for incoming data
`
GraphQL request:8:5
7 | updatedAt
8 | user {
| ^
9 | username
Traceback (most recent call last):
File "/home/tsiresy/work/python/kotkit/.venv/lib/python3.11/site-packages/graphql/execution/execute.py", line 528, in await_result
return_type, field_nodes, info, path, await result
^^^^^^^^^^^^
File "/home/tsiresy/work/python/kotkit/.venv/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 687, in _async_resolver
return await await_maybe(
^^^^^^^^^^^^^^^^^^
File "/home/tsiresy/work/python/kotkit/.venv/lib/python3.11/site-packages/strawberry/utils/await_maybe.py", line 12, in await_maybe
return await value
^^^^^^^^^^^
File "/home/tsiresy/work/python/kotkit/.venv/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 517, in resolve
related_objects = await loader.loader_for(relationship).load(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
RuntimeError: readexactly() called while another coroutine is already waiting for incoming data
`
Hey! Thanks for your great work on this!
I've queried SQLAlchemy Relationships with this library which works just fine, but fails when I use a secondary table. Example:
class Account():
...
practice = relationship("Practice", secondary="user_practice", back_populates="accounts", uselist=False)
class Practice():
...
accounts = relationship("Account", secondary="user_practice", back_populates="practice")
user_practice = Table(
...
Column("account_id", ForeignKey("account.id", ondelete="CASCADE"), primary_key=True, unique=True),
Column("practice_id", ForeignKey("practice.id", ondelete="CASCADE"), primary_key=True, index=True),
)
My query is something like this:
{
accounts {
edges {
node {
practice {
id
}
}
}
}
}
The library tries to look for account_id
in the Practice
table rather than the secondary table. I've also attached part of a stack trace below:
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 420, in resolve
related_objects = await loader.loader_for(relationship).load(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry/dataloader.py", line 251, in dispatch_batch
values = await loader.load_fn(keys)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/workspace/backend/routers/graphql/mapper.py", line 109, in load_fn
loaded = await loader.load_many(keys)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry/dataloader.py", line 251, in dispatch_batch
values = await loader.load_fn(keys)
^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 49, in load_fn
grouped_keys[group_by_remote_key(row)].append(row)
^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 41, in group_by_remote_key
[
File "/home/vscode/.local/lib/python3.11/site-packages/strawberry_sqlalchemy_mapper/loader.py", line 42, in <listcomp>
getattr(row, remote.key)
AttributeError: 'Practice' object has no attribute 'account_id'
Would be nice if docs from sqlalchemy fields form schema description
class Project(Base):
__tablename__ = "projects"
address = Column(String, nullable=False, doc="This is an address")
For some reason chatgpt is sure that it will work, but I tested it with latest version and I can't see comments from doc.
What works for me is only setting it via core stawberry manually:
name: str = strawberry.field(description="The name of the user")
but this kills the whole idea of this library since I have to write boilerplate code for every model.
Would be perfect for autodocumentation support if I can set up docs in my entites and get them converted in the schema description.
Below is the error stack
Collecting strawberry-sqlalchemy-mapper
Downloading strawberry-sqlalchemy-mapper-0.1.0.tar.gz (13 kB)
Installing build dependencies ... done
Getting requirements to build wheel ... error
error: subprocess-exited-with-error
ร Getting requirements to build wheel did not run successfully.
โ exit code: 1
โฐโ> [19 lines of output]
Traceback (most recent call last):
File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 363, in <module>
main()
File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 345, in main
json_out['return_val'] = hook(**hook_input['kwargs'])
File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\site-packages\pip\_vendor\pep517\in_process\_in_process.py", line 130, in get_requires_for_build_wheel
return hook(config_settings)
File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 338, in get_requires_for_build_wheel
return self._get_build_requires(config_settings, requirements=['wheel'])
File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 320, in _get_build_requires
self.run_setup()
File "C:\Users\user\AppData\Local\Temp\pip-build-env-nlvmub9p\overlay\Lib\site-packages\setuptools\build_meta.py", line 335, in run_setup
exec(code, locals())
File "<string>", line 14, in <module>
File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\pathlib.py", line 1135, in read_text
return f.read()
File "C:\Users\user\AppData\Local\Programs\Python\Python310\lib\encodings\cp1252.py", line 23, in decode
return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x8f in position 3209: character maps to <undefined>
[end of output]
note: This error originates from a subprocess, and is likely not a problem with pip.
error: subprocess-exited-with-error
ร Getting requirements to build wheel did not run successfully.
โ exit code: 1
โฐโ> See above for output.
It installs successfully on WSL2 running Ubuntu 22.04.1
Hello,
I was wondering if there's a way to map to an input type?
In Strawberry, you could just do:
@strawberry.type
class User:
username: str
@strawberry.input
class UserInput(User):
pass
But doing that with this library doesn't make strawberry recognize it as an input
@_strawberryMapper.type(schema.User)
class User:
pass
@strawberry.input
class UserInput(User):
pass
_strawberryMapper.finalize()
Error:
25 TypeError: UserInput fields cannot be resolved. Input field type must be a GraphQL input type.
My model has a Survey
class with owner_id
attribute, which is using a different column name (user_id
) for historic reasons
class User(Base):
__tablename__ = "user"
user_id: Mapped[int] = mapped_column("id", primary_key=True)
username: Mapped[str]
class Survey(Base):
__tablename__ = "survey"
survey_id: Mapped[int] = mapped_column("id", primary_key=True)
name: Mapped[str]
owner_id: Mapped[int] = mapped_column("user_id", ForeignKey("user.id"))
owner: Mapped[User] = relationship("User", backref="surveys", lazy=True)
import models
@strawberry_sqlalchemy_mapper.type(models.User)
class User:
pass
@strawberry_sqlalchemy_mapper.type(models.Survey)
class Survey:
pass
@strawberry.type
class Query:
@strawberry.field
def survey(self, info: Info, survey_id: int) -> typing.Optional[Survey]:
db = info.context["db"]
return db.execute(select(models.Survey).where(models.Survey.survey_id == survey_id)).scalars().first()
In relationship_resolver_for
, the code tries to access getattr(self, sql_column_name)
instead of getattr(self, python_attr_name)
query MyQuery {
survey(surveyId: 1) {
name
owner {
username
}
}
}
File ".../strawberry_sqlalchemy_mapper/mapper.py", line 409, in <listcomp>
getattr(self, local.key)
AttributeError: 'Survey' object has no attribute 'user_id'
I have a sqlalchemy model that is set up to something like this:
class UsersModel(Base):
__tablename__ = "users"
id = Column(BigInteger, unique=True, primary_key=True)
# ... other fields
and I am mapping the schema like this:
@mapper.type(UsersModel)
class Users:
...
Some of the IDs that are being used are very large, e.g.: 1_029_320_583_927
which is way beyond the limit of Int32 (2_147_483_647
), thus when I run a pretty simple query:
query {
users {
id
name
}
}
I am getting an error:
Int cannot represent non 32-bit signed integer value: 1029320583927
I think the mapping should be done to a custom scalar type in order to allow big integer values.
Unfortunately the IDs must be BigInteger since this is already being used by other services so I can't change it to be UUID or even String which might be a different approach to solve it.
I have a model, which includes column_property
class User(Base):
name = Column(Text)
lastname = Column(Text)
full_name = column_property(name + " " + lastname)
When I start application I get this stack trace:
...
File "/home/myhome/strawberry-test/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 371, in _convert_column_to_strawberry_type
if column.nullable:
File "/home/myhome/strawberry-test/lib/python3.10/site-packages/sqlalchemy/sql/elements.py", line 1495, in __getattr__
raise AttributeError(
AttributeError: Neither 'Label' object nor 'Comparator' object has an attribute 'nullable'
If I comment out the full_name
field in the model, it works as expected.
Ubuntu 22.04, python 3.10, SQLAlchemy 2.027, psycopg 3.1.18, strawberry-sqlalchemy-mapper 0.4.2, strawberry-graphql 0.219.2
I have a backend in Flask, with SQLAlchemy models and strawberry for the GraphQL. I have been using Manual GraphQL Types and converting manually the SQLalchemy models to graphQL Types. Now, I am switching to this library to ditch these GQL Types and generate them from SQLAlchemy. It works fine for almost everything. I got a strange gevent error when fetcing relationships data.
Example of my models
class Software(Base):
__tablename__ = "software"
id: Mapped[str] = mapped_column(primary_key=True, index=True)
full_name: Mapped[str] = mapped_column()
editor: Mapped[str] = mapped_column()
description: Mapped[str] = mapped_column()
language: Mapped[str] = mapped_column()
tags: Mapped[List["Tag"]] = relationship(back_populates="software", cascade="all, delete-orphan")
class Tag(Base):
__tablename__ = "tag"
id: Mapped[uuid.UUID] = mapped_column(UUID(), primary_key=True, default=uuid.uuid4)
name: Mapped[str] = mapped_column()
software_id: Mapped[str] = mapped_column(ForeignKey("software.id"), index=True)
font_color: Mapped[str] = mapped_column()
background_color: Mapped[str] = mapped_column()
software: Mapped["Software"] = relationship("Software", foreign_keys=software_id, back_populates="tags")
Gennerate types this way
# ORM MAPPING
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
@strawberry_sqlalchemy_mapper.type(SoftwareEntity)
class Software:
pass
@strawberry_sqlalchemy_mapper.type(TagEntity)
class Tag:
pass
strawberry_sqlalchemy_mapper.finalize()
additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(query=Query, mutation=Mutation, types=additional_types)
And finally I link it that way to flask:
class MyGraphQLView(GraphQLView):
init_every_request = False
config = None
session_factory = None
def get_context(self, request: Request, response: Response) -> Any:
return {
"config": MyGraphQLView.config,
"session_factory": MyGraphQLView.session_factory,
"sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=MyGraphQLView.session_factory),
}
# ...
self.engine = create_engine(self.uri, echo=False)
self.session_factory = sessionmaker(self.engine)
MyGraphQLView.config = self.config
MyGraphQLView.session_factory = self.session_factory
self.app.add_url_rule(
"/graphql",
view_func=MyGraphQLView.as_view("graphql_view", schema=schema)
)
The error I'm facing is the following:
Traceback (most recent call last):
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1478, in __call__
return self.wsgi_app(environ, start_response)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1458, in wsgi_app
response = self.handle_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 1455, in wsgi_app
response = self.full_dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 869, in full_dispatch_request
rv = self.handle_user_exception(e)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask_cors\extension.py", line 176, in wrapped_function
return cors_after_request(app.make_response(f(*args, **kwargs)))
^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 867, in full_dispatch_request
rv = self.dispatch_request()
^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\app.py", line 852, in dispatch_request
return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\flask\views.py", line 115, in view
return current_app.ensure_sync(self.dispatch_request)(**kwargs)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 100, in dispatch_request
return self.run(request=request)
^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 199, in run
result = self.execute_operation(
^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\sync_base_view.py", line 128, in execute_operation
return self.schema.execute_sync(
^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 288, in execute_sync
result = execute_sync(
^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 236, in execute_sync
ensure_future(result).cancel()
File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 649, in ensure_future
return _ensure_future(coro_or_future, loop=loop)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\Python311\Lib\asyncio\tasks.py", line 668, in _ensure_future
loop = events._get_event_loop(stacklevel=4)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\Program Files\Python311\Lib\asyncio\events.py", line 692, in get_event_loop
raise RuntimeError('There is no current event loop in thread %r.'
RuntimeError: There is no current event loop in thread 'Thread-5 (process_request_thread)'.
when running the following request in graphql
{
softwares{
id
fullName
editor
description
language
tags {
__typename
edges{
node{
id
}
}
}
}
}
If I remove the tag section in the querry, I get no error, so I guess I have an issue with relationships. From the error, I figuered it's because fetching relationship is done as an async task. However, because Flask is usin strawberry as a View, I guess It is a thread spawned for every request, but it has no event loop running on it. I figured that I would need to run a asyncio.get_event_loop()
to spawn one in this new thread, but I'm not sure where to do it.
If you guys have a better idea how to do it, or if I should just dump Flask for something not view based ?
I'll preface this with saying I am not an expert at GraphQL, so might be missing an obvious point.
Through my testing, it seems like wrapping of relationships adds 2 levels of unnecessary hierarchy in the schema. The code references "to support future pagination" but I am not privy to how this wrapping will make pagination possible and why it is not without it.
With wrapping
query {
departments {
id
name
employees {
edge { # First added level of hierarchy
node { # Second
# ...
}
}
}
}
}
}
Without wrapping
query {
departments {
id
name
employees {
# ...
}
}
}
I spent a few minutes with the code and was able to remove wrapping by adding an attribute to the StrawberrySQLAlchemyMapper
class:
def __init__(
self,
paginate_relationships: Optional[bool] = True,
model_to_type_name: Optional[Callable[[Type[BaseModelType]], str]] = None,
model_to_interface_name: Optional[Callable[[Type[BaseModelType]], str]] = None,
extra_sqlalchemy_type_to_strawberry_type_map: Optional[
Mapping[Type[TypeEngine], Type[Any]]
] = None,
) -> None:
self.paginate_relationships = paginate_relationships
# ...
Then adding a few conditionals:
def _convert_relationship_to_strawberry_type(
self, relationship: RelationshipProperty
) -> Union[Type[Any], ForwardRef]:
# ...
if relationship.uselist:
if self.paginate_relationships:
return self._connection_type_for(type_name)
else:
return List[ForwardRef(type_name)]
# ...
def connection_resolver_for(
self, relationship: RelationshipProperty
) -> Callable[..., Awaitable[Any]]:
# ...
if self.paginate_relationships and relationship.uselist:
return self.make_connection_wrapper_resolver(
relationship_resolver,
self.model_to_type_or_interface_name(relationship.entity.entity),
)
else:
return relationship_resolver
This maintains the current behavior as the default.
I do not expect this solution to be complete or even valid, but more of a proof of concept. Mostly, I am interested in learning the rationale behind the wrapping of related models.
Thanks!
After adding a relationship to two SQLAlchemy models, the following error from strawberry is produced when the models are being mapped by this package.
Traceback (most recent call last):
File "/usr/local/lib/python3.11/multiprocessing/process.py", line 314, in _bootstrap
self.run()
File "/usr/local/lib/python3.11/multiprocessing/process.py", line 108, in run
self._target(*self._args, **self._kwargs)
File "/usr/local/lib/python3.11/site-packages/uvicorn/_subprocess.py", line 76, in subprocess_started
target(sockets=sockets)
File "/usr/local/lib/python3.11/site-packages/uvicorn/server.py", line 59, in run
return asyncio.run(self.serve(sockets=sockets))
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/asyncio/runners.py", line 190, in run
return runner.run(main)
^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/asyncio/runners.py", line 118, in run
return self._loop.run_until_complete(task)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "uvloop/loop.pyx", line 1517, in uvloop.loop.Loop.run_until_complete
File "/usr/local/lib/python3.11/site-packages/uvicorn/server.py", line 66, in serve
config.load()
File "/usr/local/lib/python3.11/site-packages/uvicorn/config.py", line 471, in load
self.loaded_app = import_from_string(self.app)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/uvicorn/importer.py", line 21, in import_from_string
module = importlib.import_module(module_str)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/importlib/__init__.py", line 126, in import_module
return _bootstrap._gcd_import(name[level:], package, level)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "<frozen importlib._bootstrap>", line 1206, in _gcd_import
File "<frozen importlib._bootstrap>", line 1178, in _find_and_load
File "<frozen importlib._bootstrap>", line 1149, in _find_and_load_unlocked
File "<frozen importlib._bootstrap>", line 690, in _load_unlocked
File "<frozen importlib._bootstrap_external>", line 940, in exec_module
File "<frozen importlib._bootstrap>", line 241, in _call_with_frames_removed
File "/usr/app/run_server.py", line 8, in <module>
from src.graphql.schema import schema
File "/usr/app/src/graphql/schema.py", line 44, in <module>
schema = strawberry.federation.Schema(
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/federation/schema.py", line 73, in __init__
super().__init__(
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema.py", line 158, in __init__
raise error.__cause__ from None
File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 808, in fields
fields = resolve_thunk(self._fields)
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/graphql/type/definition.py", line 300, in resolve_thunk
return thunk() if callable(thunk) else thunk
^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 437, in <lambda>
fields=lambda: self.get_graphql_fields(object_type),
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 326, in get_graphql_fields
return self._get_thunk_mapping(
^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/local/lib/python3.11/site-packages/strawberry/schema/schema_converter.py", line 316, in _get_thunk_mapping
raise UnresolvedFieldTypeError(type_definition, field)
strawberry.exceptions.unresolved_field_type.UnresolvedFieldTypeError: Could not resolve the type of 'node'. Check that the class is accessible from the global module scope.
class Books(Base):
'''Book model.
'''
__tablename__ = 'books'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
title = Column(String(256), nullable=False)
reviews = relationship('Reviews', back_populates='book')
class Reviews(Base):
'''Review model.
'''
__tablename__ = 'reviews'
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
body = Column(String(256), nullable=False)
book_id = Column(UUID(as_uuid=True), ForeignKey('books.id'), nullable=False)
book = relationship('Books', back_populates='reviews')
strawberry_model = StrawberrySQLAlchemyMapper()
@strawberry_model.type(model=Books)
class Book:
'''Book type.
'''
__exclude__ = ['deleted_at']
strawberry_model.finalize()
First off, great job on this library overall. It has made integrating graphql with sqlalchemy a breeze.
I've run into this issue when setting up a pretty basic implementation using this library. The schema loads, but whenever I attempt to query nested relationships using the built-in dataloader sqlalchemy raises this error. I've tracked this down to the load_fn
at loader.py line 37. Simply adding a .unique()
to the call at rows = self.bind.scalars(query).all()
fixes the issue.
sqlalchemy.exc.InvalidRequestError: The unique() method must be invoked on this Result, as it contains results that include joined eager loads against collections
# raises InvalidRequestError
rows = self.bind.scalars(query).all()
# works
rows = self.bind.scalars(query).unique().all()
Hi folks!
I wanted to write something I was discussing with @bellini666 a few weeks ago, at the moment we don't really have a maintainer for this library, I think @mattalbr is quite busy with his work, so we were wondering if we need to do a call for maintainers (if you, reading this, are interested let us know!)
Additionally we were thinking about how we can converge the integrations a bit, Strawberry Django has a lot of features that can be reuse/reimplemented here, but I think some of the APIs are different, maybe there's an opportunity to update this library to have a similar API to strawberry django (of course trying to prevent breaking changes if possible)
What do you think? /cc @erikwrede
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
File "/Users/paulkluge/Documents/Work/Codebase/Active-Servers/active-servers/root-control-v2/backend/.venv/lib/python3.12/site-packages/strawberry_sqlalchemy_mapper/init.py", line 19, in
from .field import connection, field, node
File "/Users/paulkluge/Documents/Work/Codebase/Active-Servers/active-servers/root-control-v2/backend/.venv/lib/python3.12/site-packages/strawberry_sqlalchemy_mapper/field.py", line 36, in
from strawberry.arguments import StrawberryArgument, argument
ModuleNotFoundError: No module named 'strawberry.arguments'
I set up the strawberry-sqlalchemy-mapper as described, but got an error.
Looks like, strawberry-sqlalchemy-mapper 0.4.3 is not working with strawberry-graphql 0.237.3. When this bug report was written, both versions are the latest. Is this issue already known?
Python 3.12.4
Pipenv
sqlakeyset provides a neat library to do keyset-based pagination of sqlalchemy queries. I think it could be awesome if our generated connection types supported input (first, after, last, before, order, condition) and we autogenerated pagination, ordering, and filtering support. The tricky part would be some of the assumptions built into sqlakeyset (you almost always need to sort it by some primary key if the values that you're sorting aren't unique, it doesn't support sorting on nullable fields unless you coalesce them, it doesn't support sqlalchemy 2.0-style queries).
What do you think? How would you feel about this feature if someone were to implement it? With something like this, the mapper becomes really powerful and magical right off the bat, and could conceivably be used to solve real-world problems with minimal customization. As is, I have to implement every list-based relationship by hand to deal with pagination and filtering, which is a bummer. Not the end of the world, just seems like something the mapper could do on its own.
After a session.commit()
, SQLAlchemy entities can't be turned into GraphQL object because of access on outside session objects.
To explain the bug, consider this mutation:
@strawberry.type
class MutationSoftware:
@strawberry.mutation
def upsert_software(self, info, id: str, full_name: str, editor: str, description: str,
language: str) -> OBugsError | SoftwareGQL:
with info.context['session_factory']() as session:
db_software = session.query(Software).where(Software.id == id).one_or_none()
if db_software is None:
db_software = Software(id=id)
session.add(db_software)
db_software.full_name = full_name
db_software.editor = editor
db_software.description = description
db_software.language = language
session.commit()
return db_software
# return session.query(Software).where(Software.id == id).one_or_none() (1)
This code gives me this error (doesn't matter if the object is added or just updated, both cases raise the error)
Instance <Software at 0x22cdfd99410> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
GraphQL request:22:3
21 | fragment SoftwareFragment on Software {
22 | id
| ^
23 | fullName
Traceback (most recent call last):
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\graphql\execution\execute.py", line 521, in execute_field
result = resolve_fn(source, info, **args)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema_converter.py", line 541, in _get_basic_result
return field.get_result(_source, info=None, args=[], kwargs={})
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\field.py", line 212, in get_result
return self.default_resolver(source, self.python_name)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 566, in __get__
return self.impl.get(state, dict_) # type: ignore[no-any-return]
^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1086, in get
value = self._fire_loader_callables(state, key, passive)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\attributes.py", line 1116, in _fire_loader_callables
return state._load_expired(state, passive)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\state.py", line 798, in _load_expired
self.manager.expired_attribute_loader(self, toload, passive)
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\sqlalchemy\orm\loading.py", line 1558, in load_scalar_attributes
raise orm_exc.DetachedInstanceError(
sqlalchemy.orm.exc.DetachedInstanceError: Instance <Software at 0x22cdfd99410> is not bound to a Session; attribute refresh operation cannot proceed (Background on this error at: https://sqlalche.me/e/20/bhk3)
Stack (most recent call last):
File "C:\Program Files\Python311\Lib\threading.py", line 995, in _bootstrap
self._bootstrap_inner()
File "C:\Program Files\Python311\Lib\threading.py", line 1038, in _bootstrap_inner
self.run()
File "C:\Program Files\Python311\Lib\threading.py", line 975, in run
self._target(*self._args, **self._kwargs)
File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 83, in _worker
work_item.run()
File "C:\Program Files\Python311\Lib\concurrent\futures\thread.py", line 58, in run
result = self.fn(*self.args, **self.kwargs)
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 285, in _run_event_loop
loop.run_until_complete(coro)
File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 640, in run_until_complete
self.run_forever()
File "C:\Program Files\Python311\Lib\asyncio\windows_events.py", line 321, in run_forever
super().run_forever()
File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 607, in run_forever
self._run_once()
File "C:\Program Files\Python311\Lib\asyncio\base_events.py", line 1919, in _run_once
handle._run()
File "C:\Program Files\Python311\Lib\asyncio\events.py", line 80, in _run
self._context.run(self._callback, *self._args)
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\asgiref\sync.py", line 353, in main_wrap
result = await self.awaitable(*args, **kwargs)
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\flask\views.py", line 158, in dispatch_request
return await self.run(request=request)
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 186, in run
result = await self.execute_operation(
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\http\async_base_view.py", line 118, in execute_operation
return await self.schema.execute(
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\schema.py", line 256, in execute
result = await execute(
File "C:\dev\obugs-backend\src\.env\Lib\site-packages\strawberry\schema\execute.py", line 156, in execute
process_errors(result.errors, execution_context)
However, if I use the return line commented with a (1), there is no problem. Am I missing some config on the sessions or the commit?
PS C:\dev\obugs-backend\src> .\.env\Scripts\pip freeze
alembic==1.12.0
asgiref==3.7.2
blinker==1.6.2
certifi==2023.7.22
charset-normalizer==3.3.0
click==8.1.7
colorama==0.4.6
Flask==3.0.0
Flask-Cors==4.0.0
Flask-JWT-Extended==4.5.3
Flask-Mail==0.9.1
graphql-core==3.2.3
greenlet==3.0.0
idna==3.4
itsdangerous==2.1.2
Jinja2==3.1.2
Mako==1.2.4
MarkupSafe==2.1.3
psycopg2-binary==2.9.9
PyJWT==2.8.0
python-dateutil==2.8.2
requests==2.31.0
sentinel==1.0.0
six==1.16.0
SQLAlchemy==2.0.21
strawberry-graphql==0.209.5
strawberry-sqlalchemy-mapper==0.3.1
tornado==6.3.3
typing_extensions==4.8.0
urllib3==2.0.6
Werkzeug==3.0.0
Currently, imports lead to the following error (on mypy 1.10.0
):
app/graphql/schemas.py:9: error: Need type annotation for "strawberry_sqlalchemy_mapper" [var-annotated]
I've noticed that this library has a py.typed
file within it, but it appears as though it isn't being recognized
N/A
I'm struggling with some complicated resolvers for the fact that, from my understanding, the whole execution flow works without ever instantiating the strawberry types, just leveraging the fact that the fields are static methods and strawberry will take care of the execution for us.
I'd like to be more rigorous about typing and convert the return sqlalchemy models to the strawberry types before resolvers return them. Right now, the generated init seems to take every single field, and they're all positional arguments, making it hugely inconvenient. Is there a "copy constructor" of sorts to convert from ORM to Strawberry? If not, would it be hard to make one?
Currently, pagination is configured for a model automatically if any relationship contains uselist=True
, as seen in mapper.py:389. This seems undesirable as a strict rule, particularly in cases with a one-to-few relationship (think tire
s on car
s). Obviously, there are cases where pagination is desirable (such as navigating a e-commerce catalog), so pagination should be toggleable.
True
, configures the schema using relay pagination for the field, and, when False
, creates a simple array of objects for the fieldHi ๐
I really like what you have here. However, when I was trying to use it I came across some issues. Primarily, not all items in a relationship are being returned.
Here is a minimal example:
class Location(Base):
__tablename__ = "locations" # type: ignore
id: Column[int] = Column(Integer, primary_key=True, index=True)
name: Column[str] = Column(String, nullable=False, unique=True)
tasks = relationship("Task", lazy="joined", back_populates="location", uselist=False)
class Task(Base):
__tablename__ = "tasks" # type: ignore
id: Column[int] = Column(Integer, primary_key=True, index=True)
name: Column[str] = Column(String, nullable=False)
location_id: Column[Optional[int]] = Column(Integer, ForeignKey(Location.id), nullable=True)
location = relationship(Location, lazy="joined", back_populates="tasks", uselist=False)
strawberry_sqlalchemy_mapper = StrawberrySQLAlchemyMapper()
# ...
@strawberry_sqlalchemy_mapper.type(app.models.Location)
class LocationType:
pass
@strawberry_sqlalchemy_mapper.type(app.models.Task)
class TaskType:
pass
async def get_context() -> dict:
return {
"sqlalchemy_loader": StrawberrySQLAlchemyLoader(bind=SessionLocal()),
}
# ...
additional_types = list(strawberry_sqlalchemy_mapper.mapped_types.values())
schema = strawberry.Schema(Query, Mutation, extensions=[SQLAlchemySession], types=additional_types)
graphql_app = GraphQLRouter(schema, context_getter=get_context,)
strawberry_sqlalchemy_mapper.finalize()
# ...
@strawberry.type
class Query:
@strawberry.field
async def tasks(self, info: Info) -> list[TaskType]:
db = info.context["db"]
sql = db.query(app.models.Task).order_by(app.models.Task.name)
db_tasks = sql.all()
return db_tasks
@strawberry.field
async def locations(self, info: Info) -> list[LocationType]:
db = info.context["db"]
sql = db.query(app.models.Location).order_by(app.models.Location.name)
db_locations = sql.all()
return db_locations
My database contains this data:
Now when I query using GraphQl, this is the output:
Query:
query Tasks {
tasks {
id
name
location {
id
name
tasks {
id
name
}
}
}
}
Result:
{
"data": {
"tasks": [
{
"id": 1,
"name": "new Task",
"location": {
"id": 1,
"name": "New Location",
"tasks": {
"id": 4,
"name": "new Task 1-4"
}
}
},
{
"id": 2,
"name": "new Task 1-2",
"location": {
"id": 1,
"name": "New Location",
"tasks": {
"id": 4,
"name": "new Task 1-4"
}
}
},
{
"id": 3,
"name": "new Task 1-3",
"location": {
"id": 1,
"name": "New Location",
"tasks": {
"id": 4,
"name": "new Task 1-4"
}
}
},
{
"id": 4,
"name": "new Task 1-4",
"location": {
"id": 1,
"name": "New Location",
"tasks": {
"id": 4,
"name": "new Task 1-4"
}
}
},
// ...
]
}
}
I would have expected all 4 tasks would show up in the nested tasks object. I am very new to GraphQL and not a 100% sure if this is an issue with my query or it is an issue in your wrapper.
I see that the __exclude__
property is available to specify the model fields that should be excluded from the graphql API but i'm wonder how you would go the other direction and only expose the fields that explicitly specified in an __include__
list.
The reason for this is that, with the current functionality, it would be easy for a developer to add a sensitive field to the data model and forget to exclude it from the graphql schema definition, thus exposing it to the API.
Using the JSON type as a column in sqlalchemy shows as not supported for strawberry sqlalchemy mapper.
2023-08-08 12:39:13 File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 584, in convert
2023-08-08 12:39:13 self._handle_columns(mapper, type_, excluded_keys, generated_field_keys)
2023-08-08 12:39:13 File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 528, in _handle_columns
2023-08-08 12:39:13 type_annotation = self._convert_column_to_strawberry_type(column)
2023-08-08 12:39:13 File "/home/pi/.local/lib/python3.10/site-packages/strawberry_sqlalchemy_mapper/mapper.py", line 279, in _convert_column_to_strawberry_type
2023-08-08 12:39:13 raise UnsupportedColumnType(column.key, column.type)
2023-08-08 12:39:13 strawberry_sqlalchemy_mapper.exc.UnsupportedColumnType: Unsupported column type: `JSON` on column: `readingStart`. Possible fix: exclude this column
However, strawberry does already have a built-in JSON type.
from strawberry.scalars import JSON
And using this works perfectly fine when creating the type manually.
Example code:
from sqlalchemy import Column, JSON, Text
from sqlalchemy.ext.asyncio import AsyncAttrs
from sqlalchemy.orm import DeclarativeBase
from uuid import uuid4
class Base(AsyncAttrs, DeclarativeBase):
pass
class Test(Base):
__tablename__ = "test"
id = Column(Text, primary_key=True)
json = Column(JSON)
from strawberry_sqlalchemy_mapper import StrawberrySQLAlchemyMapper
_strawberryMapper = StrawberrySQLAlchemyMapper()
@_strawberryMapper.type(Test)
class StrawberryTest:
pass
_strawberryMapper.finalize()
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.