Skip to content

Instantly share code, notes, and snippets.

@mcroydon
Created May 3, 2010 05:02
Show Gist options
  • Save mcroydon/387775 to your computer and use it in GitHub Desktop.
Save mcroydon/387775 to your computer and use it in GitHub Desktop.
"""
tragic_achievements.py - A Cassandra data modeling exercise.
By `Matt Croydon <http://github.com/mcroydon>`_.
Licensed under a `BSD-style <http://www.opensource.org/licenses/bsd-license.php>`_ license.
Requires Paul Bohm's `Tragedy <http://github.com/enki/tragedy>`_.
Requires the following Keyspace definition in conf/storage-conf.xml::
<Keyspace Name="Achievements">
<ColumnFamily Name="User" CompareWith="BytesType"/>
<ColumnFamily Name="EventType" CompareWith="BytesType"/>
<ColumnFamily Name="Achievement" CompareWith="BytesType"/>
<ColumnFamily Name="AchievementCount" CompareWith="BytesType"/>
<ColumnFamily Name="Event" CompareWith="BytesType"/>
<ColumnFamily Name="EventLog" CompareWith="TimeUUIDType"/>
<ColumnFamily Name="EventTypeAchievements" CompareWith="BytesType"/>
<ColumnFamily Name="AchievementProgress" CompareWith="BytesType"/>
<ColumnFamily Name="UnlockedAchievements" CompareWith="TimeUUIDType"/>
<ReplicaPlacementStrategy>org.apache.cassandra.locator.RackUnawareStrategy</ReplicaPlacementStrategy>
<ReplicationFactor>1</ReplicationFactor>
<EndPointSnitch>org.apache.cassandra.locator.EndPointSnitch</EndPointSnitch>
</Keyspace>
Assumes a single node running on localhost.
Running the demo should yield the following::
logging event bang for matt.
logging event bang for matt.
[achievement unlocked] matt unlocked headbanger
logging event bang for matt.
logging event trip for matt.
[achievement unlocked] matt unlocked nice_trip
matt has unlocked [<Achievement nice_trip: {'created': 'Sun May 2 23:54:23 2010'}>, <Achievement headbanger: {'created': 'Sun May 2 23:54:23 2010'}>]
Events for matt: ['trip', 'bang', 'bang', 'bang']
"""
import tragedy
# Housekeeping
client = tragedy.connect(['localhost:9160'])
cluster = tragedy.Cluster('Test Cluster')
keyspace = tragedy.Keyspace('Achievements', cluster)
# Models
class User(tragedy.Model):
username = tragedy.RowKey()
created = tragedy.TimestampField(autoset_on_create=True)
@classmethod
def get_or_create(cls, username):
"""Look up an existing or create a new user."""
u = cls(row_key=username)
if not u.isComplete():
u.save()
return u
def log_event(self, event_type):
"""Log an event for a specific type."""
event = Event(user=self, event_type=event_type).save()
log = EventLog(for_user=self).append(event).save()
handle_log(self.row_key, event_type.row_key)
# Look up each achievement associated with this event type. This could get slow.
for achievement in EventTypeAchievements(for_event_type=event_type).load():
achievement.load()
unlocked_achievements = [a.row_key for a in UnlockedAchievements(for_user=self).load()]
# Only check if we haven't already unlocked this achievement
if achievement.row_key not in unlocked_achievements:
key = "%s:%s" % (self.row_key, achievement.row_key)
achievement_count = AchievementCount(row_key=key).load()
existing_count = achievement_count.get('count', 1)
achievement_count['count'] = existing_count + 1
achievement_count.save()
if achievement_count['count'] > achievement['event_count']:
# Achievement unlocked!
handle_unlock(self.row_key, achievement.row_key)
UnlockedAchievements(for_user=self).append(achievement).save()
def get_event_log(self, *args, **kwargs):
"""Get the event log for a user."""
return EventLog(for_user=self).load(*args, **kwargs).resolve()
class EventType(tragedy.Model):
"""
Holds data about different kinds of of events.
"""
name = tragedy.RowKey()
description = tragedy.StringField()
@classmethod
def get_or_create(cls, name, description):
et = cls(row_key=name).load()
if not et.isComplete():
et['description']=description
et.save()
return et
class Achievement(tragedy.Model):
"""
An achievement, including how many times what type of event has to be done to trigger it.
"""
name = tragedy.RowKey()
description = tragedy.StringField()
event_type = tragedy.ForeignKey(foreign_class=EventType)
event_count = tragedy.IntegerField()
created = tragedy.TimestampField(autoset_on_create=True)
@classmethod
def get_or_create(cls, name=name, description=description, event_type=event_type, event_count=event_count):
"""
Create an achievement and add it to the EventTypeAchievements index.
"""
achievement = cls(name=name).load()
if not achievement.isComplete():
achievement['description'] = description
achievement['event_type'] = event_type
achievement['event_count'] = event_count
achievement.save()
type_achievements = EventTypeAchievements(for_event_type=event_type).append(achievement).save()
return achievement
class AchievementCount(tragedy.Model):
"""
Holds counts for a specific user toward a specific achievement.
"""
user_and_achievement = tragedy.RowKey()
count = tragedy.IntegerField()
class Event(tragedy.Model):
"""
An event of a particular type for a user.
"""
uuid = tragedy.RowKey(autogenerate=True)
user = tragedy.ForeignKey(foreign_class=User)
event_type = tragedy.ForeignKey(foreign_class=EventType)
# Indexes
class EventLog(tragedy.Index):
"""
An event log for a user.
"""
for_user = tragedy.RowKey()
targetmodel = tragedy.ForeignKey(foreign_class=Event, compare_with='TimeUUIDType')
class EventTypeAchievements(tragedy.Index):
"""
Retrieve all achievements for an event type.
"""
for_event_type = tragedy.RowKey()
targetmodel = tragedy.ForeignKey(foreign_class=Achievement)
class UnlockedAchievements(tragedy.Index):
"""
The achievements that a user has unlocked.
"""
for_user = tragedy.RowKey()
targetmodel = tragedy.ForeignKey(foreign_class=Achievement, compare_with='TimeUUIDType')
# Callbacks
def handle_unlock(username, achievement_name):
"""
Do something when notified of an unlock.
"""
print "[achievement unlocked] %s unlocked %s" % (username, achievement_name)
def handle_log(username, event_type):
"""
Do something when logging an event.
"""
print "logging event %s for %s." % (event_type, username)
# Demo
if __name__ == '__main__':
# Your conf/storage-conf.xml should contain this snippet for the achivements keyspace
# tragedy.hacks.genconfigsnippet(keyspace)
# Verify that we're in sync with the storage configuration
keyspace.verify_datamodel()
# Lookup or create some event types
bang = EventType.get_or_create(name='bang', description='Bang your head.')
trip = EventType.get_or_create(name='trip', description='Trip on something.')
# Lookup or create some achivements
bang_head = Achievement.get_or_create(name='headbanger', description='Bang your head repeatedly', event_count=2, event_type=bang)
nice_trip = Achievement.get_or_create(name='nice_trip', description='Have a nice trip.', event_count=1, event_type=trip)
# Create a user
matt = User.get_or_create(username='matt')
# Bang your head repeatedly
for i in range(0,3):
matt.log_event(bang)
# Trip on something
matt.log_event(trip)
# Check what achievements the user has unlocked
print '%s has unlocked %s' % (matt.row_key, [x for x in UnlockedAchievements(for_user=matt).load()])
# Raw log of events for a user
print 'Events for %s: %s' % (matt.row_key, [e.load()['event_type'].row_key for e in EventLog(for_user=matt).load()])
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment