0

I am using Flask-Restful and SQLAlchemy to develop an API and am coming across a behaviour which I have a workaround for, but I am not sure the sticking with the workaround is a good long term strategy.

Pretty standard API with a List item resource with POST function and a item single resource with GET function. I tested the POST function via curl and also via a web form, and am getting the following error message:

BuildError: Could not build url for endpoint 'brand' with values ['_sa_instance_state']. Did you forget to specify values ['brand_symbol']?

The odd thing is that adding a print statement (see commented out print statement near the end of the Resource declaration below) makes the error go away.

And I know that even when the Error message is encountered

  • the item was successfully committed in the database, and
  • performing a GET on the item displays the URI perfectly fine

So based on this, I think the problem is perhaps with the POST return statement. Any help would be gratefully received - I don't want to live with a flaky workaround!

Endpoint

api.add_resource(BrandListResource, '/brands', endpoint = 'brands')
api.add_resource(BrandResource, '/brands/<string:brand_symbol>', endpoint = 'brand')

Resource

brand_fields = {
    'id': fields.Integer,
    'brand_symbol': fields.String,
    'brand_name': fields.String,
    'uri': fields.Url('brand', absolute=True)
}

class BrandResource(Resource):
    @marshal_with(brand_fields)
    def get(self, brand_symbol):
        brand = db.session.query(Brand).filter(Brand.brand_symbol == brand_symbol).first()
        if not brand:
            abort(404, message="Brand {} doesn't exist".format(brand_symbol))
        return brand

class BrandListResource(Resource):
    @marshal_with(brand_fields)
    def get(self):
        brands = db.session.query(Brand).all()
        return brands

    @marshal_with(brand_fields)
    def post(self):
        parsed_args = parser.parse_args()
        brand_symbol = parsed_args['brand_symbol']
        brand_name = parsed_args['brand_name']

        brand = db.session.query(Brand).filter(Brand.brand_symbol == brand_symbol).first()
        if brand:
            abort(404, message="Brand {} already exists".format(brand_symbol))

        brand = Brand(brand_symbol=brand_symbol, brand_name=brand_name)
        db.session.add(brand)
        db.session.commit()

        #print brand
        return brand, 201

SQLAlchemy Model

class Brand(db.Model):
    __tablename__ = 'brand'

    id = db.Column(db.Integer, primary_key=True)
    brand_symbol = db.Column(db.String(5), unique=True)
    brand_name = db.Column(db.String(200), nullable=False)

    def __init__(self, brand_symbol, brand_name):
        self.brand_symbol = brand_symbol
        self.brand_name = brand_name

    def __repr__(self):
        return '<brand_symbol {}>'.format(self.brand_symbol)

Full Error Traceback

Traceback (most recent call last):
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 2000, in __call__
    return self.wsgi_app(environ, start_response)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1991, in wsgi_app
    response = self.make_response(self.handle_exception(e))
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 271, in error_router
    return original_handler(e)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1567, in handle_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 268, in error_router
    return self.handle_error(e)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1988, in wsgi_app
    response = self.full_dispatch_request()
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1641, in full_dispatch_request
    rv = self.handle_user_exception(e)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 271, in error_router
    return original_handler(e)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1544, in handle_user_exception
    reraise(exc_type, exc_value, tb)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 268, in error_router
    return self.handle_error(e)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1639, in full_dispatch_request
    rv = self.dispatch_request()
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1625, in dispatch_request
    return self.view_functions[rule.endpoint](**req.view_args)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 477, in wrapper
    resp = resource(*args, **kwargs)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/views.py", line 84, in view
    return self.dispatch_request(*args, **kwargs)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 587, in dispatch_request
    resp = meth(*args, **kwargs)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 682, in wrapper
    return marshal(data, self.fields, self.envelope), code, headers
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 640, in marshal
    return OrderedDict([(envelope, OrderedDict(items))]) if envelope else OrderedDict(items)
  File "/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/collections.py", line 57, in __init__
    self.__update(*args, **kwds)
  File "/Users/skavie/testproject/lib/python2.7/_abcoll.py", line 571, in update
    for key, value in other:
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/__init__.py", line 639, in <genexpr>
    for k, v in fields.items())
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask_restful/fields.py", line 307, in output
    o = urlparse(url_for(endpoint, _external=self.absolute, **data))
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/helpers.py", line 332, in url_for
    return appctx.app.handle_url_build_error(error, endpoint, values)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/app.py", line 1811, in handle_url_build_error
    reraise(exc_type, exc_value, tb)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/flask/helpers.py", line 322, in url_for
    force_external=external)
  File "/Users/skavie/testproject/lib/python2.7/site-packages/werkzeug/routing.py", line 1758, in build
    raise BuildError(endpoint, values, method, self)
BuildError: Could not build url for endpoint 'brand' with values ['_sa_instance_state']. Did you forget to specify values ['brand_symbol']?
Andy G
  • 820
  • 1
  • 8
  • 11

1 Answers1

0

The brand object is expired when committing the SQLAlchemy session. The stacktrace gives a hint by pointing out that the url could not be build because there is some information missing in the object you are returning.

In this case the brand object needs to refreshed so it will contain all data again. You can do that by printing it, but it seems a bit better to use the refresh() method of the session like this:

...
db.session.commit()
db.session.refresh(brand)
return brand, 201

There is a small caveat however, the documentation of the refresh method states the following at the time of writing:

Note that a highly isolated transaction will return the same values as were previously read in that same transaction, regardless of changes in database state outside of that transaction - usage of refresh() usually only makes sense if non-ORM SQL statement were emitted in the ongoing transaction, or if autocommit mode is turned on.

This means that the information you are getting back on a refresh could be outdated, but I guess that is almost theoretical in this case.

MarcelK
  • 110
  • 1
  • 7