more documentation.
This commit is contained in:
parent
04d4bbe556
commit
b9b286b379
41
Readme.rst
41
Readme.rst
|
|
@ -1,7 +1,7 @@
|
|||
Libkosokoso -- simple tagging with sqlalchemy.
|
||||
==============================================
|
||||
|
||||
Libkosokoso provides compact and table-stingy tags to
|
||||
Libkosokoso provides compact and table-stingy tags for
|
||||
sqlalchemy-backed objects.
|
||||
|
||||
Usage
|
||||
|
|
@ -10,17 +10,22 @@ Usage
|
|||
::
|
||||
|
||||
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'
|
||||
|
||||
a = Foo()
|
||||
a.tags.append('tag1')
|
||||
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:
|
||||
db_id
|
||||
|
|
@ -58,6 +63,8 @@ provides direct access to the objects with this tag.
|
|||
|
||||
It's also possible to add kk.Tag objects directly to the tags
|
||||
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
|
||||
|
|
@ -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
|
||||
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
|
||||
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?
|
||||
----------------------
|
||||
|
||||
|
|
@ -78,6 +106,5 @@ Why is it called that?
|
|||
secretly. I picked the name because tags always feel like a kind of
|
||||
buzzing side channel to me.
|
||||
|
||||
(Originally, it was "guzuguzu", which is, like, slow. This took me
|
||||
way longer to write than I was expecting.)
|
||||
|
||||
(I also sometimes call it "guzuguzu", which is, like, slow. This took
|
||||
me way longer to write than I was expecting.)
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ class TaggableBase(object):
|
|||
|
||||
def cls_init(self, tag=None):
|
||||
# traceback.print_stack()
|
||||
# print 'cls_init called, type %s' % table
|
||||
logging.debug("cls_init called, type %s".format(table))
|
||||
super(TagAssociation, self).__init__()
|
||||
self.target_table = table
|
||||
# is this necessary? how does this get called?
|
||||
|
|
@ -191,6 +191,7 @@ class TaggableBase(object):
|
|||
|
||||
def init_base(new_base):
|
||||
"""Set up classes based on new_base."""
|
||||
# based on https://stackoverflow.com/a/41927212
|
||||
def init_cls(name, base):
|
||||
current = globals()[name + 'Base']
|
||||
new_ns = current.__dict__.copy()
|
||||
|
|
@ -202,9 +203,13 @@ def init_base(new_base):
|
|||
init_cls(c, 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(sa.orm.session.Session, 'before_flush',
|
||||
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'],
|
||||
'TagAssociation': globals()['TagAssociation'],
|
||||
'Taggable': globals()['Taggable']}
|
||||
|
|
@ -212,6 +217,7 @@ def init_base(new_base):
|
|||
|
||||
def delete_before_insert(mapper, conn, target):
|
||||
# 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'" %
|
||||
target.text)
|
||||
if r:
|
||||
|
|
|
|||
|
|
@ -84,6 +84,11 @@ class ks_basic(unittest.TestCase):
|
|||
self.session.add(t2)
|
||||
self.session.commit()
|
||||
|
||||
# del t1, t2
|
||||
ts = self.session.query(kk.Tag).all()
|
||||
self.assertEqual(1, len(ts))
|
||||
|
||||
|
||||
def test_collection(self):
|
||||
"""Test access to the collection of objects associated with a
|
||||
tag."""
|
||||
|
|
|
|||
Loading…
Reference in New Issue