marshmallow-code / flask-smorest

DB agnostic framework to build auto-documented REST APIs with Flask and marshmallow
https://flask-smorest.readthedocs.io
MIT License
651 stars 72 forks source link

SOLVED: After upgrade from 0.21 to 0.42: POST operations with DB.session.add(item) don't work #535

Closed kettenbach-it closed 1 year ago

kettenbach-it commented 1 year ago

Hi!

I am currently working on upgrading a very large API project from smorest 0.21 (and matching Flask 1, SQLAlchemy 2 etc) to the current smorest with the following relevant package versions:

My schemas look like this:

class CountrySchema(BaseSchema):
    """" Country """

    class Meta(BaseSchema.Meta):  # pylint: disable = too-few-public-methods
        """Meta"""

        model = Country
        fields = ["id", "isdeleted", "iso", "title", "continent", "eumember", "winegrower",
                  "salestax", "creationdate", "lastupdate"]
        dump_only = ["id", "isdeleted", "creationdate", "lastupdate"]

My old baseschema was inheriting from ModelSchema

class BaseSchema(ModelSchema):

which I had to change to SQLAlchemySchema - if I'm not mistaken

class BaseSchema(SQLAlchemySchema):

My controllers are like this:

    @classmethod
    @i18nblp.arguments(CountrySchema)
    @i18nblp.response(201, CountrySchema, description="Object posted successfully")
    def post(cls, item):
        """Create a Country"""
        DB.session.add(item)
        DB.session.commit()
        return item

With 0.21 and the old schema (based on ModelSchema) everything worked fine. After the upgrade, the POST operations fail (GET works) with the following error:

unit_tests/test_0021_api_full_country.py:12 (test_api_country_full)
self = <sqlalchemy.orm.session.Session object at 0x10df991b0>
instance = {'continent': 'string', 'eumember': True, 'iso': 'stringimbz', 'title': 'Test Country imbz', ...}
_warn = True

    def add(self, instance: object, _warn: bool = True) -> None:
        """Place an object into this :class:`_orm.Session`.

        Objects that are in the :term:`transient` state when passed to the
        :meth:`_orm.Session.add` method will move to the
        :term:`pending` state, until the next flush, at which point they
        will move to the :term:`persistent` state.

        Objects that are in the :term:`detached` state when passed to the
        :meth:`_orm.Session.add` method will move to the :term:`persistent`
        state directly.

        If the transaction used by the :class:`_orm.Session` is rolled back,
        objects which were transient when they were passed to
        :meth:`_orm.Session.add` will be moved back to the
        :term:`transient` state, and will no longer be present within this
        :class:`_orm.Session`.

        .. seealso::

            :meth:`_orm.Session.add_all`

            :ref:`session_adding` - at :ref:`session_basics`

        """
        if _warn and self._warn_on_events:
            self._flush_warning("Session.add()")

        try:
>           state = attributes.instance_state(instance)
E           AttributeError: 'dict' object has no attribute '_sa_instance_state'

../venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py:3335: AttributeError

The above exception was the direct cause of the following exception:

client = <FlaskClient <Flask 'Core API'>>

    @pytest.mark.options(debug=True)
    def test_api_country_full(client):
        """ Full test of country """

        randomtitle = random_string(4)

        # Create country
>       response_country_post = client.post(BASEURL + "/i18n/country",
                                            headers=PYTEST_CLIENT_DEFAULTHEADER,
                                            data=json.dumps(
                                                {
                                                    "continent": "string",
                                                    "eumember": True,
                                                    "iso": "string" + randomtitle,
                                                    "winegrower": True,
                                                    "title": "Test Country " + randomtitle
                                                }
                                            ))

unit_tests/test_0021_api_full_country.py:20: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
../venv/lib/python3.10/site-packages/werkzeug/test.py:1247: in post
    return self.open(*args, **kw)
../venv/lib/python3.10/site-packages/flask/testing.py:238: in open
    response = super().open(
../venv/lib/python3.10/site-packages/werkzeug/test.py:1196: in open
    response = self.run_wsgi_app(request.environ, buffered=buffered)
../venv/lib/python3.10/site-packages/werkzeug/test.py:1068: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
../venv/lib/python3.10/site-packages/werkzeug/test.py:1344: in run_wsgi_app
    app_rv = app(environ, start_response)
../venv/lib/python3.10/site-packages/flask/app.py:2552: in __call__
    return self.wsgi_app(environ, start_response)
../venv/lib/python3.10/site-packages/werkzeug/middleware/proxy_fix.py:182: in __call__
    return self.app(environ, start_response)
../venv/lib/python3.10/site-packages/flask/app.py:2532: in wsgi_app
    response = self.handle_exception(e)
../venv/lib/python3.10/site-packages/flask/app.py:2529: in wsgi_app
    response = self.full_dispatch_request()
../venv/lib/python3.10/site-packages/flask/app.py:1825: in full_dispatch_request
    rv = self.handle_user_exception(e)
../venv/lib/python3.10/site-packages/flask/app.py:1823: in full_dispatch_request
    rv = self.dispatch_request()
../venv/lib/python3.10/site-packages/flask/app.py:1799: in dispatch_request
    return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args)
../venv/lib/python3.10/site-packages/flask/views.py:107: in view
    return current_app.ensure_sync(self.dispatch_request)(**kwargs)
../venv/lib/python3.10/site-packages/flask/views.py:188: in dispatch_request
    return current_app.ensure_sync(meth)(**kwargs)
../venv/lib/python3.10/site-packages/webargs/core.py:649: in wrapper
    return func(*args, **kwargs)
../venv/lib/python3.10/site-packages/flask_smorest/arguments.py:82: in wrapper
    return func(*f_args, **f_kwargs)
../venv/lib/python3.10/site-packages/flask_smorest/response.py:89: in wrapper
    func(*args, **kwargs)
../controller/i18n.py:78: in post
    DB.session.add(item)
../venv/lib/python3.10/site-packages/sqlalchemy/orm/scoping.py:375: in add
    return self._proxied.add(instance, _warn=_warn)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <sqlalchemy.orm.session.Session object at 0x10df991b0>
instance = {'continent': 'string', 'eumember': True, 'iso': 'stringimbz', 'title': 'Test Country imbz', ...}
_warn = True

    def add(self, instance: object, _warn: bool = True) -> None:
        """Place an object into this :class:`_orm.Session`.

        Objects that are in the :term:`transient` state when passed to the
        :meth:`_orm.Session.add` method will move to the
        :term:`pending` state, until the next flush, at which point they
        will move to the :term:`persistent` state.

        Objects that are in the :term:`detached` state when passed to the
        :meth:`_orm.Session.add` method will move to the :term:`persistent`
        state directly.

        If the transaction used by the :class:`_orm.Session` is rolled back,
        objects which were transient when they were passed to
        :meth:`_orm.Session.add` will be moved back to the
        :term:`transient` state, and will no longer be present within this
        :class:`_orm.Session`.

        .. seealso::

            :meth:`_orm.Session.add_all`

            :ref:`session_adding` - at :ref:`session_basics`

        """
        if _warn and self._warn_on_events:
            self._flush_warning("Session.add()")

        try:
            state = attributes.instance_state(instance)
        except exc.NO_STATE as err:
>           raise exc.UnmappedInstanceError(instance) from err
E           sqlalchemy.orm.exc.UnmappedInstanceError: Class 'builtins.dict' is not mapped

../venv/lib/python3.10/site-packages/sqlalchemy/orm/session.py:3337: UnmappedInstanceError

What I observe:

The type of item fed to post() is <dict>. In the old software it was: <class 'models.country.Country'> and I think this is the reason why the add() fails.

What do I have to add, to (automatically) make "Country" objects while deserializing?

Can I keep my schemas as they are, given that my baseschema inherits the SQLAlchemySchema?

kettenbach-it commented 1 year ago

Solution:

load_instance=Trueneeds to be set in class Meta