pythonsqlalchemypyramidgeoalchemy2

Geometry('POINT') column being returned as str object


I have an sqlalchemy model object which has the following column:

gps = Column(Geometry('POINT'))

I have implemented a to_dict function in the model class, for which I need to deconstruct the gps object to give me lat and long. This successfully works for me in another model. But for some reason, in the class in question, the following piece of code results in an attribute error ('str' object has no attribute 'data'):

point = wkb.loads(bytes(self.gps.data))

I store the gps data like so:

gps = Point(longitude, latitude).wkt

Here's the table description from postgresql:

   Column    |            Type             |                     Modifiers                     | Storage | Stats target | Description 
-------------+-----------------------------+---------------------------------------------------+---------+--------------+-------------
 id          | integer                     | not null default nextval('pins_id_seq'::regclass) | plain   |              | 
 gps         | geometry(Point)             |                                                   | main    |              | 

I am calling the as dict method as soon as the Pin object gets created like so:

gps = Point(
        float(data['longitude']),
        float(data['latitude'])
    ).wkt
pin = Pin(gps=gps)
# Commit pin to disk 
# otherwise fields will 
# not return properly
with transaction.manager:
    self.dbsession.add(pin)
    transaction.commit()
    print (pin.as_dict())

What's driving me insane is the fact that the exact some code works for the other model. Any insight would be mucho appreciated.

Edit: Following Ilja's comment, I understood that the issue is that the object isn't getting written to the disk, and apparently the Geometry column will get treated as a string till that happens. But I am getting the same error even now. Basically, at this point, the transaction.commit() function isn't doing what I think it is supposed to...

Relevant to that is the configuration of the session object. Since all this is under the Pyramid web framework, I am using the default session configuration, as described here (you can skip the first few paragraphs, until they start discussing the /models/__init__.py file. Ctrl + F if need be).

In case I have left some important detail out, reproducing the problematic class here below:

from geoalchemy2 import Geometry
from sqlalchemy import (
    Column,
    Integer,
)
from shapely import wkb

from .meta import Base


class Pin(Base):
    __tablename__ = 'pins'
    id = Column(Integer, primary_key=True)
    gps = Column(Geometry('POINT'))

    def as_dict(self):
        toret = {}
        point = wkb.loads(bytes(self.gps.data))
        lat = point.x
        lon = point.y
        toret['gps'] = {'lon': lon, 'lat': lat}
        return toret

Solution

  • At first I thought that the cause of the

    Traceback (most recent call last):
      ...
      File "/.../pyramid_test/views/default.py", line 28, in my_view
        print(pin.as_dict())
      File "/.../pyramid_test/models/pin.py", line 18, in as_dict
        point = wkb.loads(bytes(self.gps.data))
    AttributeError: 'str' object has no attribute 'data'
    

    was that zope.sqlalchemy closes the session on commit, but leaves instances unexpired, but that was not the case. This was due to having used Pyramid some time ago when the global transaction would still affect the ongoing transaction during a request, but now the default seems to be an explicit transaction manager.

    The actual problem is that transaction.commit() has no effect on the ongoing transaction of the current session. Adding some logging will make this clear:

    with transaction.manager:
        self.dbsession.add(pin)
        transaction.commit()
        print("Called transaction.commit()")
        insp = inspect(pin)
        print(insp.transient,
              insp.pending,
              insp.persistent,
              insp.detached,
              insp.deleted,
              insp.session)
    

    which results in about:

     % env/bin/pserve development.ini   
    2018-01-19 14:36:25,113 INFO  [shapely.speedups._speedups:219][MainThread] Numpy was not imported, continuing without requires()
    Starting server in PID 1081.
    Serving on http://localhost:6543
    ...
    Called transaction.commit()
    False True False False False <sqlalchemy.orm.session.Session object at 0x7f958169d0f0>
    ...
    2018-01-19 14:36:28,855 INFO  [sqlalchemy.engine.base.Engine:682][waitress] BEGIN (implicit)
    2018-01-19 14:36:28,856 INFO  [sqlalchemy.engine.base.Engine:1151][waitress] INSERT INTO pins (gps) VALUES (ST_GeomFromEWKT(%(gps)s)) RETURNING pins.id
    2018-01-19 14:36:28,856 INFO  [sqlalchemy.engine.base.Engine:1154][waitress] {'gps': 'POINT (1 1)'}
    2018-01-19 14:36:28,881 INFO  [sqlalchemy.engine.base.Engine:722][waitress] COMMIT
    

    As can be seen no commit takes place and the instance is still in pending state, and so its gps attribute holds the text value from the assignment. If you wish to handle your serialization the way you do, you could first flush the changes to the DB and then expire the instance attribute(s):

    gps = Point(
            float(data['longitude']),
            float(data['latitude'])
        ).wkt
    pin = Pin(gps=gps)
    self.dbsession.add(pin)
    self.dbsession.flush()
    self.dbsession.expire(pin, ['gps'])  # expire the gps attr
    print(pin.as_dict())  # SQLAlchemy will fetch the value from the DB
    

    On the other hand you could also avoid having to handle the (E)WKB representation in the application and request the coordinates from the DB directly using for example column_property() accessors:

    class Pin(Base):
        __tablename__ = 'pins'
        id = Column(Integer, primary_key=True)
        gps = Column(Geometry('POINT'))
        gps_x = column_property(gps.ST_X())
        gps_y = column_property(gps.ST_Y())
    
        def as_dict(self):
            toret = {}
            toret['gps'] = {'lon': self.gps_y, 'lat': self.gps_x}
            return toret
    

    With that the manual expire(pin) becomes unnecessary, since the column properties have to refresh themselves anyway in this case. And of course since you already know your coordinates when you're constructing the new Pin, you could just prefill them:

    lon = float(data['longitude'])
    lat = float(data['latitude'])
    gps = Point(lon, lat).wkt
    pin = Pin(gps=gps, gps_x=lat, gps_y=lon)
    

    and so no flushing, expiring, and fetching is even needed.