more documentation.
This commit is contained in:
parent
04d4bbe556
commit
b9b286b379
43
Readme.rst
43
Readme.rst
|
|
@ -1,7 +1,7 @@
|
||||||
Libkosokoso -- simple tagging with sqlalchemy.
|
Libkosokoso -- simple tagging with sqlalchemy.
|
||||||
==============================================
|
==============================================
|
||||||
|
|
||||||
Libkosokoso provides compact and table-stingy tags to
|
Libkosokoso provides compact and table-stingy tags for
|
||||||
sqlalchemy-backed objects.
|
sqlalchemy-backed objects.
|
||||||
|
|
||||||
Usage
|
Usage
|
||||||
|
|
@ -10,17 +10,22 @@ Usage
|
||||||
::
|
::
|
||||||
|
|
||||||
import libkosokoso as kk
|
import libkosokoso as kk
|
||||||
|
import sqlalchemy as sa
|
||||||
|
base = sa.ext.declarative.declarative_base()
|
||||||
|
|
||||||
class Foo(kk.Taggable):
|
kk.__dict__.update(kk.init_base(base))
|
||||||
|
|
||||||
|
class Foo(kk.Taggable, base):
|
||||||
__tablename__ = 'foos'
|
__tablename__ = 'foos'
|
||||||
|
|
||||||
a = Foo()
|
a = Foo()
|
||||||
a.tags.append('tag1')
|
a.tags.append('tag1')
|
||||||
a.tags.extend(['tag2', 'tag3'])
|
a.tags.extend(['tag2', 'tag3'])
|
||||||
|
|
||||||
and so forth.
|
and so forth. (Note that certain key steps, like connecting to a
|
||||||
|
database, are omitted.)
|
||||||
|
|
||||||
This will generate tables like this::
|
The code above will generate tables like this::
|
||||||
|
|
||||||
foos:
|
foos:
|
||||||
db_id
|
db_id
|
||||||
|
|
@ -57,7 +62,9 @@ objects. The tag object has a useful member, "collection", which
|
||||||
provides direct access to the objects with this tag.
|
provides direct access to the objects with this tag.
|
||||||
|
|
||||||
It's also possible to add kk.Tag objects directly to the tags
|
It's also possible to add kk.Tag objects directly to the tags
|
||||||
property on a taggable object, and they'll be handled correctly.
|
property on a taggable object, and they'll be handled correctly.
|
||||||
|
Similarly, it's possible to add objects to the collections member of a
|
||||||
|
Tag object.
|
||||||
|
|
||||||
|
|
||||||
Design considerations
|
Design considerations
|
||||||
|
|
@ -67,10 +74,31 @@ My basic reasoning was that at some point I'd have to go and muck
|
||||||
about in the database by hand, and I wanted a structure that was easy
|
about in the database by hand, and I wanted a structure that was easy
|
||||||
to perceive and modify.
|
to perceive and modify.
|
||||||
|
|
||||||
|
I also wanted to avoid modifying preexisting tables. This structure
|
||||||
|
can be dropped into a live system without interfering with any current
|
||||||
|
entities. (Likewise the reverse.)
|
||||||
|
|
||||||
|
At all times I aimed to have an interface that matches my intuitions.
|
||||||
|
Hence things like being able to both add tags to an object and add
|
||||||
|
objects to a tag, despite the massive increase in complexity for
|
||||||
|
arguably minimal gain.
|
||||||
|
|
||||||
I explicitly did not aim for efficiency. The underlying code is
|
I explicitly did not aim for efficiency. The underlying code is
|
||||||
object-happy and probably very inefficient.
|
object-happy and probably very inefficient.
|
||||||
|
|
||||||
|
|
||||||
|
Implementation notes
|
||||||
|
--------------------
|
||||||
|
|
||||||
|
The init_base() call required to set up the library is an absolute
|
||||||
|
mess. I'm pleased that I got it working, but kind of surprised.
|
||||||
|
|
||||||
|
The authors of SQLAlchemy would most likely be appalled. This model
|
||||||
|
breaks the "relational" element of the database quite badly. One
|
||||||
|
solution that might make everyone happy would be to add a tags table
|
||||||
|
for every current type, and use a view to consolidate them if desired.
|
||||||
|
|
||||||
|
|
||||||
Why is it called that?
|
Why is it called that?
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
@ -78,6 +106,5 @@ Why is it called that?
|
||||||
secretly. I picked the name because tags always feel like a kind of
|
secretly. I picked the name because tags always feel like a kind of
|
||||||
buzzing side channel to me.
|
buzzing side channel to me.
|
||||||
|
|
||||||
(Originally, it was "guzuguzu", which is, like, slow. This took me
|
(I also sometimes call it "guzuguzu", which is, like, slow. This took
|
||||||
way longer to write than I was expecting.)
|
me way longer to write than I was expecting.)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -154,7 +154,7 @@ class TaggableBase(object):
|
||||||
|
|
||||||
def cls_init(self, tag=None):
|
def cls_init(self, tag=None):
|
||||||
# traceback.print_stack()
|
# traceback.print_stack()
|
||||||
# print 'cls_init called, type %s' % table
|
logging.debug("cls_init called, type %s".format(table))
|
||||||
super(TagAssociation, self).__init__()
|
super(TagAssociation, self).__init__()
|
||||||
self.target_table = table
|
self.target_table = table
|
||||||
# is this necessary? how does this get called?
|
# is this necessary? how does this get called?
|
||||||
|
|
@ -191,6 +191,7 @@ class TaggableBase(object):
|
||||||
|
|
||||||
def init_base(new_base):
|
def init_base(new_base):
|
||||||
"""Set up classes based on new_base."""
|
"""Set up classes based on new_base."""
|
||||||
|
# based on https://stackoverflow.com/a/41927212
|
||||||
def init_cls(name, base):
|
def init_cls(name, base):
|
||||||
current = globals()[name + 'Base']
|
current = globals()[name + 'Base']
|
||||||
new_ns = current.__dict__.copy()
|
new_ns = current.__dict__.copy()
|
||||||
|
|
@ -202,9 +203,13 @@ def init_base(new_base):
|
||||||
init_cls(c, new_base)
|
init_cls(c, new_base)
|
||||||
|
|
||||||
globals()['Base'] = new_base
|
globals()['Base'] = new_base
|
||||||
|
# these are here because "Tag" needs to exist before adding the
|
||||||
|
# listener.
|
||||||
sa.event.listen(Tag, 'before_insert', delete_before_insert)
|
sa.event.listen(Tag, 'before_insert', delete_before_insert)
|
||||||
sa.event.listen(sa.orm.session.Session, 'before_flush',
|
sa.event.listen(sa.orm.session.Session, 'before_flush',
|
||||||
enforce_unique_text)
|
enforce_unique_text)
|
||||||
|
# need to return new types so that the caller can incorporate them
|
||||||
|
# into its view of the module.
|
||||||
return {'Tag': globals()['Tag'],
|
return {'Tag': globals()['Tag'],
|
||||||
'TagAssociation': globals()['TagAssociation'],
|
'TagAssociation': globals()['TagAssociation'],
|
||||||
'Taggable': globals()['Taggable']}
|
'Taggable': globals()['Taggable']}
|
||||||
|
|
@ -212,6 +217,7 @@ def init_base(new_base):
|
||||||
|
|
||||||
def delete_before_insert(mapper, conn, target):
|
def delete_before_insert(mapper, conn, target):
|
||||||
# TODO figure out where exactly transactions happen.
|
# TODO figure out where exactly transactions happen.
|
||||||
|
# TODO can we just upsert? Or, for that matter, skip the insert?
|
||||||
r = conn.execute("SELECT db_id FROM kk_tags WHERE text='%s'" %
|
r = conn.execute("SELECT db_id FROM kk_tags WHERE text='%s'" %
|
||||||
target.text)
|
target.text)
|
||||||
if r:
|
if r:
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,11 @@ class ks_basic(unittest.TestCase):
|
||||||
self.session.add(t2)
|
self.session.add(t2)
|
||||||
self.session.commit()
|
self.session.commit()
|
||||||
|
|
||||||
|
# del t1, t2
|
||||||
|
ts = self.session.query(kk.Tag).all()
|
||||||
|
self.assertEqual(1, len(ts))
|
||||||
|
|
||||||
|
|
||||||
def test_collection(self):
|
def test_collection(self):
|
||||||
"""Test access to the collection of objects associated with a
|
"""Test access to the collection of objects associated with a
|
||||||
tag."""
|
tag."""
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue