Hi,

Thank you for the elaborated Answer !

I am trying to implement a general solution for the key->list problem
using events.

basically i want to instrument for GroupByKeyCollection any changes
relevant to the keyfunc.

say we have
p= Person()
p._address_by_role['r1']= [PersonToAddress(address=Address(name='a1'),
role='r1')  ]

My problem is that i cannot access the parent object (PersonToAddress)
from ScalarAttributeImpl supplied by the events framework as
initiation parameter of the set callback. What i want is to remove an
object from a key-associated list when it's keying function result
mutates. For this i have to fetch the PersonToAddress from
PersonToAddress.role.set event. Can you hint me a way to fetch a
mapped object from it's attribute set event ?

The following describes how i see path to the solution. Do you think i
am on the right track?

The keying function shall be reapplied whenever keying Attributes are
mutated on PersonToAddress. Upon detecting a changed value i want to
reorganize the _address_by_role structure.

The second step would be to implement callbacks on the instrumented
lists which form the values of the GroupByKeyCollection.

The behavior i target is such that:

p._address_by_role.append(PersonToAddress(address=Address(name='a1',
role='r1')) #OK
p._address_by_role['r2'].append(PersonToAddress(address=Address(name='a1'),
role='r1')) # OK, but  PersonToAddress.role is changed to 'r2'
p._address_by_role['r2'].append(PersonToAddress(address=Address(name='a1')))
#OK,  PersonToAddress.role is set to 'r2'
del p._address_by_role['r2'][0] #O.K, the first element is removed,
and it's role value is set to the default value

p._address_by_role['r2'][1]=  p._address_by_role['r1'][0]
# OK, but may steps should happen here:
#   -p._address_by_role['r1'][0] is put into p._address_by_role['r2']
#   -this changes the attr. value p._address_by_role['r1'][0].role to "r2"
#   this triggers the removal from p._address_by_role['r1']

Thank you
Paul

2013/8/13 Michael Bayer <mike...@zzzcomputing.com>:
>
> On Aug 13, 2013, at 11:44 AM, Paul Balomiri <paulbalom...@gmail.com> wrote:
>
>> I would like to get a list as value for the dict, such that i can
>> assign more than one entity to any one key. The output should look
>> like this:
>> {u'home': [<Address object at 0x29568d0>,<Address object at ...>] ,
>> u'work': [<Address object at 0x2a3eb90>]}
>>
>> Now in the database whenever i set a new value for a key(=role), the
>> entry in PersonToAddress' table is replaced (not added). This is
>> consistent with having a 1-key to 1-value mapping. Can I however
>> change the behaviour in such a way that more than one Addresses are
>> allowed for one Person using the same key(=role in this example)?
>>
>
> OK, an attribute_mapped_collection is just an adapter for what is basically a 
> sequence.  Instead of a sequence of objects, it's a sequence of (key, 
> object).   So by itself, attribute_mapped_collection can only store mapped 
> objects, not collections as values.
>
> When using the association proxy, there is a way to get a dictionary of 
> values, but the association proxy only knows how to close two "hops" into 
> one.  So to achieve that directly, you'd need one relationship that is a 
> key/value mapping to a middle object, then that middle object has a 
> collection of things.    So here PersonToAddress would be more like 
> PersonAddressCollection, and then each Address object would have a 
> person_address_collection_id.   That's obviously not the traditional 
> association object pattern - instead of a collection of associations to 
> scalars, it's a collection of collections, since that's really the structure 
> you're looking for here.
>
> To approximate the "collection of collections" on top of a traditional 
> association pattern is tricky.  The simplest way is probably to make a 
> read-only @property that just fabricates a dictionary of collections on the 
> fly, reading from the pure collection of PersonToAddress objects.  If you 
> want just a quick read-only system, I'd go with that.
>
> Otherwise, we need to crack open the collection mechanics completely, and 
> since you want association proxying, we need to crack that open as well.  
> I've worked up a proof of concept for this idea which is below, and it was 
> not at all trivial to come up with.  In particular I stopped at getting 
> Person.addresses_by_role['role'].append(Address()) to work, since that means 
> we'd need two distinctly instrumented collections, it's doable but is more 
> complex.    Below I adapted collections.defaultdict() to provide us with a 
> "collection of collections" over a single collection and also the association 
> proxy's base collection adapter in order to reduce the hops:
>
> from sqlalchemy import *
> from sqlalchemy.orm import *
> from sqlalchemy.ext.declarative import declarative_base
> import collections
> from sqlalchemy.orm.collections import collection, collection_adapter
> from sqlalchemy.ext.associationproxy import association_proxy, 
> _AssociationCollection
> Base = declarative_base()
>
> class GroupByKeyCollection(collections.defaultdict):
>     def __init__(self, keyfunc):
>         super(GroupByKeyCollection, self).__init__(list)
>         self.keyfunc = keyfunc
>
>     @collection.appender
>     def add(self, value, _sa_initiator=None):
>         key = self.keyfunc(value)
>         self[key].append(value)
>
>     @collection.remover
>     def remove(self, value, _sa_initiator=None):
>         key = self.keyfunc(value)
>         self[key].remove(value)
>
>     @collection.internally_instrumented
>     def __setitem__(self, key, value):
>         adapter = collection_adapter(self)
>         # the collection API usually provides these events transparently, but 
> due to
>         # the unusual structure, we pretty much have to fire them ourselves
>         # for each item.
>         for item in value:
>             item = adapter.fire_append_event(item, None)
>         collections.defaultdict.__setitem__(self, key, value)
>
>     @collection.internally_instrumented
>     def __delitem__(self, key, value):
>         adapter = collection_adapter(self)
>         for item in value:
>             item = adapter.fire_remove_event(item, None)
>         collections.defaultdict.__delitem__(self, key, value)
>
>     @collection.iterator
>     def iterate(self):
>         for collection in self.values():
>             for item in collection:
>                 yield item
>
>     @collection.converter
>     def _convert(self, target):
>         for collection in target.values():
>             for item in collection:
>                 yield item
>
>     def update(self, k):
>         raise NotImplementedError()
>
>
> class AssociationGBK(_AssociationCollection):
>     def __init__(self, lazy_collection, creator, value_attr, parent):
>         getter, setter = parent._default_getset(parent.collection_class)
>         super(AssociationGBK, self).__init__(
>                 lazy_collection, creator, getter, setter, parent)
>
>     def _create(self, key, value):
>         return self.creator(key, value)
>
>     def _get(self, object):
>         return self.getter(object)
>
>     def _set(self, object, key, value):
>         return self.setter(object, key, value)
>
>     def __getitem__(self, key):
>         return [self._get(item) for item in self.col[key]]
>
>     def __setitem__(self, key, value):
>         self.col[key] = [self._create(key, item) for item in value]
>
>     def add(self, key, item):
>         self.col.add(self._create(key, item))
>
>     def items(self):
>         return ((key, [self._get(item) for item in self.col[key]])
>                 for key in self.col)
>
>     def update(self, kw):
>         for key, value in kw.items():
>             self[key] = value
>
>     def clear(self):
>         self.col.clear()
>
>     def copy(self):
>         return dict(self.items())
>
>     def __repr__(self):
>         return repr(dict(self.items()))
>
>
> class Person(Base):
>     __tablename__ = 'person'
>
>     id = Column(Integer, primary_key=True)
>
>     _addresses_by_role = relationship("PersonToAddress",
>                             collection_class=
>                             lambda: GroupByKeyCollection(
>                                         keyfunc=lambda item: item.role
>                                     )
>                         )
>     addresses_by_role = association_proxy(
>                             "_addresses_by_role",
>                             "address",
>                             proxy_factory=AssociationGBK,
>                             creator=lambda k, v: PersonToAddress(role=k, 
> address=v))
>
> class PersonToAddress(Base):
>     __tablename__ = 'person_to_address'
>
>     id = Column(Integer, primary_key=True)
>     person_id = Column(Integer, ForeignKey('person.id'))
>     address_id = Column(Integer, ForeignKey('address.id'))
>     role = Column(String)
>     address = relationship("Address")
>
> class Address(Base):
>     __tablename__ = 'address'
>
>     id = Column(Integer, primary_key=True)
>     name = Column(String)
>
> e = create_engine("sqlite://", echo=True)
> Base.metadata.create_all(e)
>
> sess = Session(e)
>
> p1 = Person(addresses_by_role={
>     "r1": [
>         Address(name='a1'),
>         Address(name='a2')
>     ],
>     "r2": [
>         Address(name='a3')
>     ]
> })
>
> sess.add(p1)
>
> sess.commit()
>
> print(p1.addresses_by_role)
>
> # to get p1.addresses_by_role['r3'].append(Address()) to work,
> # we'd need to also instrument the lists inside of the mapping....
> p1.addresses_by_role.add('r3', Address(name='r3'))
>
> print(p1.addresses_by_role)
>
> sess.commit()
>
> print(p1.addresses_by_role)
>
>



-- 
paulbalom...@gmail.com

-- 
You received this message because you are subscribed to the Google Groups 
"sqlalchemy" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to sqlalchemy+unsubscr...@googlegroups.com.
To post to this group, send email to sqlalchemy@googlegroups.com.
Visit this group at http://groups.google.com/group/sqlalchemy.
For more options, visit https://groups.google.com/groups/opt_out.

Reply via email to