AttributeError: объект типа «Пользователь» не имеет атрибута «_query_cls»

User.id имел тип postgresql.UUID Message.sender_id имел тип postgresql.UUID с внешним ключом на User.id.

Изменил мой тип на sqlalchemy_util.UUIDType.

У меня возникла проблема с сериализацией моего внешнего ключа, поэтому я установил свой собственный JSONEncoder Теперь все работает правильно, за исключением создания Message (у других классов с такой же конфигурацией проблем нет).

test_message.py

def test_create_message(client, db, admin_user, admin_headers):
    # test bad data
    data = {
        'message': 'foobar',
        'title': 'title',
    }
    rep = client.post(
        '/api/v1/messages',
        json=data,
        headers=admin_headers
    )
    assert rep.status_code == 422

    data['sender'] = admin_user.id.hex
    data['recipient'] = admin_user.id.hex

    rep = client.post(
        '/api/v1/messages',
        json=data,
        headers=admin_headers
    )
    assert rep.status_code == 201

    data = rep.get_json()
    message = db.session.query(Message).filter_by(id=data['message']['id']).first()

    assert message.message == 'foobar'
    assert message.title == 'title'

models/message.py

from sqlalchemy_utils import UUIDType

from supervisor.extensions import db

from .notification import Notification


class Message(Notification):
    """Basic message model
    """
    __tablename__ = 'message'
    __repr_attrs__ = ['sender', 'message']
    id = db.Column(
        db.Integer,
        db.ForeignKey('notification.id', ondelete='CASCADE'),
        primary_key=True
    )
    sender_id = db.Column(
        UUIDType(binary=False),
        db.ForeignKey('user.id', ondelete='CASCADE'),
    )
    sender = db.relationship(
        'User',
        lazy='joined',
    )
    message = db.Column(db.Text, nullable=False)

    def __init__(self, **kwargs):
        kwargs.setdefault('type', 'message')
        super().__init__(**kwargs)

модели/user.py

class User(BaseModel):
    """Basic user model
    """
    __tablename__ = 'user'
    __repr_attrs__ = ['email']
    id = db.Column(UUIDType(binary=False), default=uuid.uuid4, primary_key=True)
    email = db.Column(db.String(128), unique=True, nullable=False)
    name = db.Column(db.String(128))
    photo_url = db.Column(db.Text)
    active = db.Column(db.Boolean, default=True)
    company_id = db.Column(
        db.Integer,
        db.ForeignKey('company.id', ondelete='CASCADE'),
        nullable=False
    )
    admin_id = db.Column(
        db.Integer,
        db.ForeignKey('company.id'),
        nullable=True
    )
    sites = db.relationship(
        'Site',
        secondary='users_sites',
        lazy='joined',
    )
    tel_office = db.Column(db.String(25))
    title = db.Column(db.String(128))
    supplies = db.relationship(
        'Supply',
        lazy='joined',
        backref='contact'
    )
    notifications = db.relationship(
        'Notification',
        lazy='joined',
        backref='recipient',
        foreign_keys='[Notification.recipient_id]',
        cascade='all,delete-orphan',
        passive_deletes=True,
    )

модели/base.py

from sqlalchemy_mixins import AllFeaturesMixin, ReprMixin

from supervisor.extensions import db


class BaseModel(db.Model, AllFeaturesMixin, ReprMixin):
    __abstract__ = True
    __repr__ = ReprMixin.__repr__

    def save(self):
        db.session.add(self)
        db.session.commit()

    def delete(self):
        db.session.delete(self)
        db.session.commit()

результаты набора тестов

_______________________________________________________________________________________________ test_create_message _______________________________________________________________________________________________

client = <FlaskClient <Flask 'supervisor'>>, db = <SQLAlchemy engine=postgresql+psycopg2://postgres:***@/rework-product-test?host=/cloudsql/flowlity:europe-west1:supervisor-db-test>
admin_user = <User #a7b838c3a8e64d118b13b1b28ab41f5f 'admin@flowlity....'>
admin_headers = {'authorization': 'Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1Njc0Mzg0MDgsIm5iZiI6MTU2NzQzODQwOCwianRpIjo...yZXNoIjpmYWxzZSwidHlwZSI6ImFjY2VzcyJ9.Zd5pOLAR5YSHDugU52E4qN-xQmXoqyJZTR_LNEbsUYM', 'content-type': 'application/json'}

    def test_create_message(client, db, admin_user, admin_headers):
        # test bad data
        data = {
            'message': 'foobar',
            'title': 'title',
        }
        rep = client.post(
            '/api/v1/messages',
            json=data,
            headers=admin_headers
        )
        assert rep.status_code == 422

        data['sender'] = admin_user.id.hex
        data['recipient'] = admin_user.id.hex

        rep = client.post(
            '/api/v1/messages',
            json=data,
>           headers=admin_headers
        )

tests/test_message.py:35: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
.tox/py37/lib/python3.7/site-packages/werkzeug/test.py:1039: in post
    return self.open(*args, **kw)
.tox/py37/lib/python3.7/site-packages/flask/testing.py:227: in open
    follow_redirects=follow_redirects,
.tox/py37/lib/python3.7/site-packages/werkzeug/test.py:993: in open
    response = self.run_wsgi_app(environ.copy(), buffered=buffered)
.tox/py37/lib/python3.7/site-packages/werkzeug/test.py:884: in run_wsgi_app
    rv = run_wsgi_app(self.application, environ, buffered=buffered)
.tox/py37/lib/python3.7/site-packages/werkzeug/test.py:1119: in run_wsgi_app
    app_rv = app(environ, start_response)
.tox/py37/lib/python3.7/site-packages/flask/app.py:2463: in __call__
    return self.wsgi_app(environ, start_response)
.tox/py37/lib/python3.7/site-packages/flask_socketio/__init__.py:46: in __call__
    start_response)
.tox/py37/lib/python3.7/site-packages/engineio/middleware.py:74: in __call__
    return self.wsgi_app(environ, start_response)
.tox/py37/lib/python3.7/site-packages/flask/app.py:2449: in wsgi_app
    response = self.handle_exception(e)
.tox/py37/lib/python3.7/site-packages/flask_restful/__init__.py:269: in error_router
    return original_handler(e)
.tox/py37/lib/python3.7/site-packages/flask_cors/extension.py:161: in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
.tox/py37/lib/python3.7/site-packages/flask/app.py:1866: in handle_exception
    reraise(exc_type, exc_value, tb)
.tox/py37/lib/python3.7/site-packages/flask/_compat.py:38: in reraise
    raise value.with_traceback(tb)
.tox/py37/lib/python3.7/site-packages/flask/app.py:2446: in wsgi_app
    response = self.full_dispatch_request()
.tox/py37/lib/python3.7/site-packages/flask/app.py:1951: in full_dispatch_request
    rv = self.handle_user_exception(e)
.tox/py37/lib/python3.7/site-packages/flask_restful/__init__.py:269: in error_router
    return original_handler(e)
.tox/py37/lib/python3.7/site-packages/flask_cors/extension.py:161: in wrapped_function
    return cors_after_request(app.make_response(f(*args, **kwargs)))
.tox/py37/lib/python3.7/site-packages/flask/app.py:1820: in handle_user_exception
    reraise(exc_type, exc_value, tb)
.tox/py37/lib/python3.7/site-packages/flask/_compat.py:38: in reraise
    raise value.with_traceback(tb)
.tox/py37/lib/python3.7/site-packages/flask/app.py:1949: in full_dispatch_request
    rv = self.dispatch_request()
.tox/py37/lib/python3.7/site-packages/flask/app.py:1935: in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
.tox/py37/lib/python3.7/site-packages/flask_restful/__init__.py:458: in wrapper
    resp = resource(*args, **kwargs)
.tox/py37/lib/python3.7/site-packages/flask/views.py:89: in view
    return self.dispatch_request(*args, **kwargs)
.tox/py37/lib/python3.7/site-packages/flask_restful/__init__.py:573: in dispatch_request
    resp = meth(*args, **kwargs)
.tox/py37/lib/python3.7/site-packages/flask_jwt_extended/view_decorators.py:103: in wrapper
    return fn(*args, **kwargs)
supervisor/api/resources/message.py:51: in post
    message = schema.load(request.json)
.tox/py37/lib/python3.7/site-packages/marshmallow_sqlalchemy/schema.py:216: in load
    return super(ModelSchema, self).load(data, *args, **kwargs)
.tox/py37/lib/python3.7/site-packages/marshmallow/schema.py:684: in load
    data, many=many, partial=partial, unknown=unknown, postprocess=True
.tox/py37/lib/python3.7/site-packages/marshmallow/schema.py:799: in _do_load
    unknown=unknown,
.tox/py37/lib/python3.7/site-packages/marshmallow/schema.py:639: in _deserialize
    index=index,
.tox/py37/lib/python3.7/site-packages/marshmallow/schema.py:483: in _call_and_store
    value = getter_func(data)
.tox/py37/lib/python3.7/site-packages/marshmallow/schema.py:632: in <lambda>
    val, field_name, data, **d_kwargs
.tox/py37/lib/python3.7/site-packages/marshmallow/fields.py:329: in deserialize
    output = self._deserialize(value, attr, data, **kwargs)
.tox/py37/lib/python3.7/site-packages/marshmallow_sqlalchemy/fields.py:109: in _deserialize
    self.session.query(self.related_model), value
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <class 'supervisor.models.user.User'>, entities = (), kwargs = {}

    def query(self, *entities, **kwargs):
        """Return a new :class:`.Query` object corresponding to this
        :class:`.Session`."""

>       return self._query_cls(entities, self, **kwargs)
E       AttributeError: type object 'User' has no attribute '_query_cls'

.tox/py37/lib/python3.7/site-packages/sqlalchemy/orm/session.py:1544: AttributeError
---------------------------------------------------------------------------------------------- Captured stdout call -----------------------------------------------------------------------------------------------
{'message': 'foobar', 'title': 'title'}
{'message': 'foobar', 'recipient': 'a7b838c3a8e64d118b13b1b28ab41f5f', 'sender': 'a7b838c3a8e64d118b13b1b28ab41f5f', 'title': 'title'}

person Altiire    schedule 02.09.2019    source источник
comment
В вашей модели User нет атрибута _query_cls. Не могли бы вы включить его определение в вопрос?   -  person SuperShoot    schedule 03.09.2019
comment
@SuperShoot Я добавил свой класс пользователя   -  person Altiire    schedule 03.09.2019
comment
И какое определение для BaseModel?   -  person SuperShoot    schedule 03.09.2019
comment
Я добавил свой базовый класс. sqlalchemy_mixins был для CRUD, но у меня были ошибки с сохранением/удалением, поэтому я реализовал свой собственный   -  person Altiire    schedule 03.09.2019
comment
ОК, кажется, я приближаюсь, вы создаете объект marshmallow-sqlalchemy ModelSchema где-то в своем коде и передаете ему сеанс sqlalchemy?   -  person SuperShoot    schedule 03.09.2019
comment
Да, каждая модель имеет свою собственную конечную точку API, а объекты управляются ModelSchema, инициированной моделью и sql_session. Вам нужен исходный код ModelSchema для Message/User?   -  person Altiire    schedule 03.09.2019
comment
ХОРОШО. Похоже, вы передаете класс Session экземплярам схемы вместо экземпляра Session(). Причина, по которой я говорю это, заключается в том, что метод, из которого возникает ошибка, — это Session.query, но self в контексте этой функции — это ваш класс User. Итак, когда sqlalchemy-marshmallow делает это: self.session.query(self.related_model), self.related_user, который является вашим классом User, передается параметру self метода Session.query. Если бы ModelSchema.session был экземпляром Session(), то self был бы этим экземпляром. Надеюсь, я объяснил, что хорошо.   -  person SuperShoot    schedule 03.09.2019
comment
Omg эти опечатки... Я передал db.Session вместо db.session...   -  person Altiire    schedule 03.09.2019


Ответы (1)


Как сообщает SuperShoot,

Моя ModelSchema была ошибочно создана:

class MessageSchema(ma.ModelSchema):

    class Meta:
        model = Message
        sqla_session = db.Session

Ответ был исправить

        sqla_session = db.Session

to

        sqla_session = db.session
person Altiire    schedule 03.09.2019