443 lines
11 KiB
Python
443 lines
11 KiB
Python
#!/usr/bin/env python
|
|
|
|
from tornado.gen import coroutine, Return
|
|
|
|
from ..db import model
|
|
from enum import Enum
|
|
|
|
|
|
class TraitType(Enum):
|
|
"""
|
|
Trait types
|
|
"""
|
|
SINGLETON = 'singleton'
|
|
AVATAR_HASH = 'avatar_hash'
|
|
STRING = 'string'
|
|
PAIR = 'pair'
|
|
|
|
|
|
class DatabaseShutdown(object):
|
|
"""
|
|
Lives in the "thread local" storage and tells us when
|
|
that gets 'freed' so that we can close the database connection.
|
|
"""
|
|
def __init__(self, db):
|
|
self._db = db
|
|
|
|
def __del__(self):
|
|
self._db.close()
|
|
|
|
|
|
class Trait(object):
|
|
"""
|
|
A base class for all Traits
|
|
"""
|
|
|
|
# All known traits
|
|
_ALL_TRAITS = {}
|
|
|
|
def __init__(self, trait, app, log):
|
|
# This is a singleton class
|
|
assert not hasattr(self.__class__, '_instance')
|
|
assert self._TRAIT_CLASS not in self._ALL_TRAITS
|
|
|
|
self._trait = trait
|
|
self._log = log.getChild(self._TRAIT_CLASS)
|
|
self._app = app
|
|
self._db = app._db
|
|
self._ALL_TRAITS[self._TRAIT_CLASS] = self
|
|
|
|
@classmethod
|
|
@coroutine
|
|
def init(cls, app, log):
|
|
db = app._db
|
|
# Fetch the trait instance
|
|
trait = yield model.Trait.fetch(
|
|
db, 'trait_class=%s', cls._TRAIT_CLASS,
|
|
single=True)
|
|
|
|
if trait is None:
|
|
# Create the trait
|
|
yield db.query('''
|
|
INSERT INTO "trait"
|
|
(trait_class, trait_type, score, count, weight)
|
|
VALUES
|
|
(%s, %s, 0, 0, 1.0)
|
|
''',
|
|
cls._TRAIT_CLASS,
|
|
cls._TRAIT_TYPE.value,
|
|
commit=True)
|
|
|
|
# Fetch the trait instance
|
|
trait = yield model.Trait.fetch(
|
|
db, 'trait_class=%s', cls._TRAIT_CLASS,
|
|
single=True)
|
|
assert cls(trait, app, log)
|
|
|
|
@property
|
|
def trait_id(self):
|
|
return self._trait.trait_id
|
|
|
|
@property
|
|
def weight(self):
|
|
return self._trait.weight
|
|
|
|
@classmethod
|
|
@coroutine
|
|
def assess(cls, user, log):
|
|
"""
|
|
Assess the given user for their traits.
|
|
"""
|
|
assert isinstance(user, model.User)
|
|
user_traits = []
|
|
|
|
for trait in list(cls._ALL_TRAITS.values()):
|
|
trait_log = log.getChild(trait._TRAIT_CLASS)
|
|
trait_log.audit('Assessing %s', user)
|
|
try:
|
|
user_trait = yield trait._assess(
|
|
user, trait_log)
|
|
except:
|
|
trait_log.exception('Failed to assess %s for trait %s',
|
|
user, trait)
|
|
continue
|
|
|
|
if user_trait is not None:
|
|
user_traits.append(user_trait)
|
|
log.audit('User %s has %s', user, user_traits)
|
|
raise Return(user_traits)
|
|
|
|
@coroutine
|
|
def _assess(self, user, log):
|
|
"""
|
|
Assess the user against this particular trait.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
|
|
class StringTrait(Trait):
|
|
"""
|
|
Helper sub-class for string-based traits.
|
|
"""
|
|
_TRAIT_TYPE = TraitType.STRING
|
|
|
|
@coroutine
|
|
def _get_trait_instance(self, value):
|
|
# See if the instance exists
|
|
trait_instance = yield model.TraitInstanceString.fetch(
|
|
self._db,
|
|
'trait_id=%s AND trait_string=%s LIMIT 1',
|
|
self.trait_id, value, single=True
|
|
)
|
|
|
|
if trait_instance is None:
|
|
# Create the instance.
|
|
yield self._db.query('''
|
|
INSERT INTO "trait_instance"
|
|
(trait_id, trait_string, score, count)
|
|
VALUES
|
|
(%s, %s, 0, 0)
|
|
''', self.trait_id, value, commit=True)
|
|
|
|
trait_instance = yield model.TraitInstanceString.fetch(
|
|
self._db,
|
|
'trait_id=%s AND trait_string=%s',
|
|
self.trait_id, value, single=True
|
|
)
|
|
raise Return(TraitInstance(self, trait_instance))
|
|
|
|
|
|
class BaseTraitInstance(object):
|
|
"""
|
|
An instance of a given trait, linked to a user.
|
|
"""
|
|
def __init__(self, trait):
|
|
assert isinstance(trait, Trait)
|
|
self._trait = trait
|
|
|
|
@property
|
|
def score(self):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def count(self):
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def trait(self):
|
|
return self._trait
|
|
|
|
@property
|
|
def instance(self):
|
|
raise NotImplementedError()
|
|
|
|
@coroutine
|
|
def increment(self, count, direction):
|
|
"""
|
|
Increment the score and count.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@property
|
|
def _db(self):
|
|
return self._trait._db
|
|
|
|
|
|
class BaseUserTraitInstance(object):
|
|
"""
|
|
An instance of a trait linked to a user.
|
|
"""
|
|
def __init__(self, user, trait_instance, count):
|
|
assert isinstance(trait_instance, BaseTraitInstance)
|
|
self._user_id = user.user_id
|
|
self._trait_instance = trait_instance
|
|
self._count = count
|
|
self._user_trait_id = None
|
|
|
|
@property
|
|
def _user_trait(self):
|
|
return NotImplementedError()
|
|
|
|
@property
|
|
def count(self):
|
|
return self._count
|
|
|
|
@property
|
|
def weighted_score(self):
|
|
if self.trait_count == 0:
|
|
return 0.0
|
|
|
|
return (float(self.trait_score) * self.trait.weight) \
|
|
/ float(self.trait_count)
|
|
|
|
@property
|
|
def trait_score(self):
|
|
return self._trait_instance.score
|
|
|
|
@property
|
|
def trait_count(self):
|
|
return self._trait_instance.count
|
|
|
|
@property
|
|
def trait(self):
|
|
return self._trait_instance.trait
|
|
|
|
@property
|
|
def instance(self):
|
|
return self._trait_instance.instance
|
|
|
|
@coroutine
|
|
def discard(self):
|
|
"""
|
|
Remove a link between a trait and a user from the database.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@coroutine
|
|
def persist(self):
|
|
"""
|
|
Persist this user trait instance count in the database.
|
|
"""
|
|
raise NotImplementedError()
|
|
|
|
@coroutine
|
|
def increment_trait(self, direction):
|
|
"""
|
|
Increment the trait instance score
|
|
"""
|
|
yield self._trait_instance.increment(
|
|
self.count, direction)
|
|
|
|
@property
|
|
def _db(self):
|
|
return self._trait_instance._db
|
|
|
|
|
|
class TraitInstance(BaseTraitInstance):
|
|
"""
|
|
An instance of a given trait.
|
|
"""
|
|
def __init__(self, trait, instance):
|
|
super(TraitInstance, self).__init__(trait)
|
|
self._instance = instance
|
|
self._log = trait._log.getChild('inst[%s]' % instance.instance)
|
|
|
|
@property
|
|
def trait_inst_id(self):
|
|
return self._instance.trait_inst_id
|
|
|
|
@property
|
|
def score(self):
|
|
return self._instance.score
|
|
|
|
@property
|
|
def count(self):
|
|
return self._instance.count
|
|
|
|
@property
|
|
def instance(self):
|
|
return self._instance.instance
|
|
|
|
@coroutine
|
|
def increment(self, count, direction):
|
|
"""
|
|
Increment the score and count.
|
|
"""
|
|
if count == 0:
|
|
return
|
|
|
|
yield self._instance.refresh()
|
|
self._instance.score += (count * direction)
|
|
self._instance.count += count
|
|
yield self._instance.commit()
|
|
self._log.debug('Adjust instance score=%d count=%d',
|
|
self._instance.score, self._instance.count)
|
|
|
|
|
|
class UserTraitInstance(BaseUserTraitInstance):
|
|
"""
|
|
An instance of a trait linked to a user.
|
|
"""
|
|
@coroutine
|
|
def discard(self):
|
|
yield self._db.query('''
|
|
DELETE FROM "user_trait_instance"
|
|
WHERE
|
|
user_id=%s
|
|
AND
|
|
trait_inst_id=%s
|
|
''',
|
|
self._user_id,
|
|
self._trait_instance.trait_inst_id,
|
|
commit=True)
|
|
|
|
@coroutine
|
|
def persist(self):
|
|
"""
|
|
Persist this user trait instance count in the database.
|
|
"""
|
|
yield self._db.query('''
|
|
INSERT INTO "user_trait_instance"
|
|
(user_id, trait_inst_id, count)
|
|
VALUES
|
|
(%s, %s, %s)
|
|
ON CONFLICT DO UPDATE
|
|
SET
|
|
count=%s
|
|
WHERE
|
|
user_id=%s
|
|
AND
|
|
trait_inst_id=%s
|
|
''',
|
|
self._user_id,
|
|
self._trait_instance.trait_inst_id,
|
|
self.count,
|
|
self.count,
|
|
self._user_id,
|
|
self._trait_instance.trait_inst_id,
|
|
commit=True)
|
|
|
|
|
|
class SingletonTrait(Trait):
|
|
"""
|
|
A trait which is a singleton.
|
|
"""
|
|
_TRAIT_TYPE = TraitType.SINGLETON
|
|
|
|
@property
|
|
def score(self):
|
|
return self._trait.score
|
|
|
|
@property
|
|
def count(self):
|
|
return self._trait.count
|
|
|
|
@coroutine
|
|
def increment(self, count, direction):
|
|
"""
|
|
Increment the score and count.
|
|
"""
|
|
if count == 0:
|
|
return
|
|
|
|
yield self._trait.refresh()
|
|
self._trait.score += (count * direction)
|
|
self._trait.count += count
|
|
yield self._trait.commit()
|
|
self._log.debug('Adjust trait score=%d count=%d',
|
|
self._trait.score, self._trait.count)
|
|
|
|
|
|
class SingletonTraitInstance(BaseTraitInstance):
|
|
"""
|
|
An instance of a given singleton trait.
|
|
"""
|
|
def __init__(self, trait):
|
|
super(SingletonTraitInstance, self).__init__(trait)
|
|
|
|
@property
|
|
def score(self):
|
|
return self._trait.score
|
|
|
|
@property
|
|
def count(self):
|
|
return self._trait.count
|
|
|
|
@property
|
|
def instance(self):
|
|
return None
|
|
|
|
def increment(self, count, direction):
|
|
"""
|
|
Increment the score and count.
|
|
"""
|
|
self._trait.increment(count, direction)
|
|
|
|
|
|
class UserSingletonTraitInstance(BaseUserTraitInstance):
|
|
"""
|
|
A singleton trait linked to a user.
|
|
"""
|
|
|
|
@coroutine
|
|
def discard(self):
|
|
"""
|
|
Remove a link between a trait and a user from the database.
|
|
"""
|
|
yield self._db.query('''
|
|
DELETE FROM "user_trait"
|
|
WHERE
|
|
user_id=%s
|
|
AND
|
|
trait_id=%s
|
|
''',
|
|
self._user_id,
|
|
self._trait_instance.trait.trait_id,
|
|
commit=True)
|
|
|
|
@coroutine
|
|
def persist(self):
|
|
"""
|
|
Persist this user trait instance count in the database.
|
|
"""
|
|
yield self._db.query('''
|
|
INSERT INTO "user_trait"
|
|
(user_id, trait_id, count)
|
|
VALUES
|
|
(%s, %s, %s)
|
|
ON CONFLICT DO UPDATE
|
|
SET
|
|
count=%s
|
|
WHERE
|
|
user_id=%s
|
|
AND
|
|
trait_id=%s
|
|
''',
|
|
self._user_id,
|
|
self._trait_instance.trait.trait_id,
|
|
self.count,
|
|
self.count,
|
|
self._user_id,
|
|
self._trait_instance.trait.trait_id,
|
|
commit=True)
|