more documentation.

This commit is contained in:
chris t 2020-07-20 19:07:53 -07:00
parent 04d4bbe556
commit b9b286b379
3 changed files with 47 additions and 9 deletions

View File

@ -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.)

View File

@ -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:

View File

@ -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."""