Created
May 3, 2010 05:02
-
-
Save mcroydon/387775 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
""" | |
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