modeling many-to-many relationships
This commit is contained in:
+26
-1
@@ -1,3 +1,4 @@
|
||||
import enum
|
||||
import logging
|
||||
import nanoid
|
||||
from nanoid_dictionary import human_alphabet
|
||||
@@ -33,7 +34,11 @@ class IterableMixin:
|
||||
yield attr, values[attr]
|
||||
for relname in self.__mapper__.relationships.keys():
|
||||
relvals = []
|
||||
for rel in self.__getattribute__(relname):
|
||||
reliter = self.__getattribute__(relname)
|
||||
if not reliter:
|
||||
yield relname, relvals
|
||||
continue
|
||||
for rel in reliter:
|
||||
try:
|
||||
relvals.append({k: v for k, v in vars(rel).items() if not k.startswith('_')})
|
||||
except TypeError:
|
||||
@@ -49,6 +54,9 @@ class IterableMixin:
|
||||
serialized[key] = value
|
||||
return serialized
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self))
|
||||
|
||||
|
||||
def multivalue_string_factory(name, column=Column(String), separator=';'):
|
||||
"""
|
||||
@@ -76,5 +84,22 @@ def multivalue_string_factory(name, column=Column(String), separator=';'):
|
||||
})
|
||||
|
||||
|
||||
class EnumField(enum.Enum):
|
||||
"""
|
||||
A serializable enum.
|
||||
"""
|
||||
def __json__(self, request):
|
||||
return self.value
|
||||
|
||||
|
||||
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
||||
SkillsMixin = multivalue_string_factory('skills')
|
||||
|
||||
STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
||||
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
||||
CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
||||
StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
|
||||
|
||||
# class Table(*Bases):
|
||||
Bases = [BaseObject, IterableMixin, SlugMixin]
|
||||
|
||||
+36
-22
@@ -49,15 +49,40 @@ data = {
|
||||
{'id': 1, 'name': 'human', 'creature_type': 'humanoid'},
|
||||
{'id': 2, 'name': 'dragonborn', 'creature_type': 'humanoid'},
|
||||
{'id': 3, 'name': 'tiefling', 'creature_type': 'humanoid'},
|
||||
{'id': 4, 'name': 'elf', 'creature_type': 'humanoid'},
|
||||
],
|
||||
|
||||
'AncestryTrait': [
|
||||
{ 'id': 1, 'name': '+1 to All Ability Scores', },
|
||||
{ 'id': 2, 'name': 'Breath Weapon', },
|
||||
{ 'id': 3, 'name': 'Darkvision', },
|
||||
],
|
||||
|
||||
'AncestryTraitMap': [
|
||||
{ 'ancestry_id': 1, 'ancestry_trait_id': 1, 'level': 1}, # human +1 to scores
|
||||
{ 'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # dragonborn breath weapon
|
||||
{ 'ancestry_id': 3, 'ancestry_trait_id': 3, 'level': 1}, # tiefling darkvision
|
||||
{ 'ancestry_id': 2, 'ancestry_trait_id': 2, 'level': 1}, # elf darkvision
|
||||
],
|
||||
|
||||
'CharacterClassMap': [
|
||||
{
|
||||
'character_id': 1,
|
||||
'character_class_id': 1,
|
||||
'level': 2,
|
||||
},
|
||||
{
|
||||
'character_id': 1,
|
||||
'character_class_id': 2,
|
||||
'level': 3,
|
||||
},
|
||||
],
|
||||
|
||||
'Character': [
|
||||
{
|
||||
'id': 1,
|
||||
'name': 'Sabetha',
|
||||
'ancestry': 'human',
|
||||
'character_class': ['fighter', 'rogue'],
|
||||
'level': 1,
|
||||
'ancestry_id': 1,
|
||||
'armor_class': 10,
|
||||
'max_hit_points': 14,
|
||||
'hit_points': 14,
|
||||
@@ -76,29 +101,18 @@ data = {
|
||||
],
|
||||
|
||||
'ClassAttribute': [
|
||||
{
|
||||
'character_class_id': 1,
|
||||
'name': 'Fighting Style',
|
||||
'value': 'Archery',
|
||||
'level': 1,
|
||||
},
|
||||
{'id': 1, 'name': 'Fighting Style', 'value': 'Archery'},
|
||||
],
|
||||
|
||||
'AncestryTrait': [
|
||||
{
|
||||
'id': 1,
|
||||
'ancestry_id': 1,
|
||||
'name': '+1 to All Ability Scores',
|
||||
'level': 1,
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'ancestry_id': 2,
|
||||
'name': 'Breath Weapon',
|
||||
'level': 1,
|
||||
},
|
||||
'ClassAttributeMap': [
|
||||
{'class_attribute_id': 1, 'character_class_id': 1, 'level': 2}, # Fighter: Archery fighting style
|
||||
],
|
||||
|
||||
'CharacterClassAttributeMap': [
|
||||
{'class_attribute_id': 1, 'character_id': 1}, # Sabetha: Archery fighting style
|
||||
],
|
||||
|
||||
|
||||
'Modifier': [
|
||||
# Humans
|
||||
{'source_table_name': 'ancestry_trait', 'source_table_id': 1, 'value': '+1', 'type': 'stat', 'target': 'str'},
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
import enum
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy import UniqueConstraint
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
||||
from ttfrog.db.base import multivalue_string_factory
|
||||
|
||||
|
||||
STATS = ['STR', 'DEX', 'CON', 'INT', 'WIS', 'CHA']
|
||||
|
||||
CREATURE_TYPES = ['aberation', 'beast', 'celestial', 'construct', 'dragon', 'elemental', 'fey', 'fiend', 'Giant',
|
||||
'humanoid', 'monstrosity', 'ooze', 'plant', 'undead']
|
||||
|
||||
|
||||
class EnumField(enum.Enum):
|
||||
"""
|
||||
A serializable enum.
|
||||
"""
|
||||
def __json__(self, request):
|
||||
return self.value
|
||||
|
||||
|
||||
# enums for db schemas
|
||||
StatsEnum = EnumField("StatsEnum", ((k, k) for k in STATS))
|
||||
CreatureTypesEnum = EnumField("CreatureTypesEnum", ((k, k) for k in CREATURE_TYPES))
|
||||
|
||||
CharacterClassMixin = multivalue_string_factory('character_class', Column(String, nullable=False))
|
||||
SavingThrowsMixin = multivalue_string_factory('saving_throws')
|
||||
SkillsMixin = multivalue_string_factory('skills')
|
||||
|
||||
|
||||
class Skill(*Bases):
|
||||
__tablename__ = "skill"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
description = Column(Text)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Proficiency(*Bases):
|
||||
__tablename__ = "proficiency"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Ancestry(*Bases):
|
||||
__tablename__ = "ancestry"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
creature_type = Column(Enum(CreatureTypesEnum))
|
||||
traits = relationship("AncestryTrait")
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class AncestryTrait(BaseObject, IterableMixin):
|
||||
__tablename__ = "ancestry_trait"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||
|
||||
|
||||
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character_class"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
hit_dice = Column(String, default='1d6')
|
||||
hit_dice_stat = Column(Enum(StatsEnum))
|
||||
proficiencies = Column(String)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class ClassAttribute(BaseObject, IterableMixin):
|
||||
__tablename__ = "class_attribute"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
|
||||
name = Column(String, nullable=False)
|
||||
value = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Character(*Bases, CharacterClassMixin, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
ancestry = Column(String, ForeignKey("ancestry.name"), nullable=False, default='human')
|
||||
name = Column(String, default='New Character', nullable=False)
|
||||
level = Column(Integer, default=1, nullable=False, info={'min': 1, 'max': 20})
|
||||
armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99})
|
||||
hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||
max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||
temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0, 'max': 999})
|
||||
speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
|
||||
str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
int = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
wis = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
cha = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
proficiencies = Column(String)
|
||||
|
||||
|
||||
class Modifier(BaseObject, IterableMixin):
|
||||
__tablename__ = "modifier"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('source_table_name', 'source_table_id', 'value', 'type', 'target'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
source_table_id = Column(Integer, index=True, nullable=False)
|
||||
value = Column(String, nullable=False)
|
||||
type = Column(String, nullable=False)
|
||||
target = Column(String, nullable=False)
|
||||
|
||||
|
||||
class TransactionLog(BaseObject, IterableMixin):
|
||||
__tablename__ = "transaction_log"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
primary_key = Column(Integer, index=True)
|
||||
diff = Column(Text)
|
||||
@@ -0,0 +1,4 @@
|
||||
from .character import *
|
||||
from .classes import *
|
||||
from .property import *
|
||||
from .transaction import *
|
||||
@@ -0,0 +1,91 @@
|
||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
|
||||
from ttfrog.db.base import CreatureTypesEnum
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Ancestry',
|
||||
'AncestryTrait',
|
||||
'AncestryTraitMap',
|
||||
'CharacterClassMap',
|
||||
'CharacterClassAttributeMap',
|
||||
'Character',
|
||||
]
|
||||
|
||||
|
||||
class AncestryTraitMap(BaseObject):
|
||||
__tablename__ = "trait_map"
|
||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), primary_key=True)
|
||||
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"), primary_key=True)
|
||||
trait = relationship("AncestryTrait", lazy='immediate')
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20})
|
||||
|
||||
|
||||
class Ancestry(*Bases):
|
||||
"""
|
||||
A character ancestry ("race"), which has zero or more AncestryTraits.
|
||||
"""
|
||||
__tablename__ = "ancestry"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
creature_type = Column(Enum(CreatureTypesEnum))
|
||||
traits = relationship("AncestryTraitMap", lazy='immediate')
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AncestryTrait(BaseObject, IterableMixin):
|
||||
"""
|
||||
A trait granted to a character via its Ancestry.
|
||||
"""
|
||||
__tablename__ = "ancestry_trait"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
|
||||
|
||||
class CharacterClassMap(BaseObject):
|
||||
__tablename__ = "class_map"
|
||||
character_id = Column(Integer, ForeignKey("character.id"), primary_key=True)
|
||||
character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
|
||||
character_class = relationship("CharacterClass", lazy='immediate')
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
|
||||
|
||||
|
||||
class CharacterClassAttributeMap(BaseObject):
|
||||
__tablename__ = "character_class_attribute_map"
|
||||
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
|
||||
character_id = Column(Integer, ForeignKey("character.id"), primary_key=True)
|
||||
attribute = relationship("ClassAttribute", lazy='immediate')
|
||||
|
||||
|
||||
class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, default='New Character', nullable=False)
|
||||
armor_class = Column(Integer, default=10, nullable=False, info={'min': 1, 'max': 99})
|
||||
hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||
max_hit_points = Column(Integer, default=1, nullable=False, info={'min': 0, 'max': 999})
|
||||
temp_hit_points = Column(Integer, default=0, nullable=False, info={'min': 0, 'max': 999})
|
||||
speed = Column(Integer, nullable=False, default=30, info={'min': 0, 'max': 99})
|
||||
str = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
dex = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
con = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
int = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
wis = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
cha = Column(Integer, nullable=False, default=10, info={'min': 0, 'max': 30})
|
||||
proficiencies = Column(String)
|
||||
|
||||
classes = relationship("CharacterClassMap")
|
||||
attributes = relationship("CharacterClassAttributeMap")
|
||||
|
||||
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default='1')
|
||||
ancestry = relationship("Ancestry", uselist=False)
|
||||
@@ -0,0 +1,43 @@
|
||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin, SavingThrowsMixin, SkillsMixin
|
||||
from ttfrog.db.base import StatsEnum
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Enum
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy import ForeignKey
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
|
||||
__all__ = [
|
||||
'ClassAttributeMap',
|
||||
'ClassAttribute',
|
||||
'CharacterClass',
|
||||
]
|
||||
|
||||
|
||||
class ClassAttributeMap(BaseObject):
|
||||
__tablename__ = "class_attribute_map"
|
||||
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), primary_key=True)
|
||||
character_class_id = Column(Integer, ForeignKey("character_class.id"), primary_key=True)
|
||||
attribute = relationship("ClassAttribute", lazy='immediate')
|
||||
level = Column(Integer, nullable=False, info={'min': 1, 'max': 20}, default=1)
|
||||
|
||||
|
||||
class ClassAttribute(BaseObject, IterableMixin):
|
||||
__tablename__ = "class_attribute"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
value = Column(String, nullable=False)
|
||||
description = Column(Text)
|
||||
|
||||
|
||||
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character_class"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
hit_dice = Column(String, default='1d6')
|
||||
hit_dice_stat = Column(Enum(StatsEnum))
|
||||
proficiencies = Column(String)
|
||||
attributes = relationship("ClassAttributeMap")
|
||||
@@ -0,0 +1,46 @@
|
||||
from ttfrog.db.base import Bases, BaseObject, IterableMixin
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Text
|
||||
from sqlalchemy import UniqueConstraint
|
||||
|
||||
|
||||
__all__ = [
|
||||
'Skill',
|
||||
'Proficiency',
|
||||
'Modifier',
|
||||
]
|
||||
|
||||
|
||||
class Skill(*Bases):
|
||||
__tablename__ = "skill"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
description = Column(Text)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Proficiency(*Bases):
|
||||
__tablename__ = "proficiency"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
|
||||
def __repr__(self):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Modifier(BaseObject, IterableMixin):
|
||||
__tablename__ = "modifier"
|
||||
__table_args__ = (
|
||||
UniqueConstraint('source_table_name', 'source_table_id', 'value', 'type', 'target'),
|
||||
)
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
source_table_id = Column(Integer, index=True, nullable=False)
|
||||
value = Column(String, nullable=False)
|
||||
type = Column(String, nullable=False)
|
||||
target = Column(String, nullable=False)
|
||||
@@ -0,0 +1,14 @@
|
||||
from ttfrog.db.base import BaseObject, IterableMixin
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy import Integer
|
||||
from sqlalchemy import String
|
||||
from sqlalchemy import Text
|
||||
|
||||
__all__ = ['TransactionLog']
|
||||
|
||||
class TransactionLog(BaseObject, IterableMixin):
|
||||
__tablename__ = "transaction_log"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
primary_key = Column(Integer, index=True)
|
||||
diff = Column(Text)
|
||||
Reference in New Issue
Block a user