convert to modern MappedAsDataclass models

This commit is contained in:
evilchili 2024-04-28 14:30:47 -07:00
parent 1ff0e5ca7d
commit 3980be5f07
6 changed files with 143 additions and 100 deletions

View File

@ -2,9 +2,9 @@ import enum
import nanoid
from nanoid_dictionary import human_alphabet
from pyramid_sqlalchemy import BaseObject as _BaseObject
from slugify import slugify
from sqlalchemy import Column, String
from sqlalchemy.orm import DeclarativeBase, MappedAsDataclass
def genslug():
@ -19,7 +19,7 @@ class SlugMixin:
return "-".join([self.slug, slugify(self.name.title().replace(" ", ""), ok="", only_ascii=True, lower=False)])
class BaseObject(_BaseObject):
class BaseObject(MappedAsDataclass, DeclarativeBase):
"""
Allows for iterating over Model objects' column names and values
"""

View File

@ -7,30 +7,30 @@ def bootstrap():
db.init()
with db.transaction():
# ancestries
human = schema.Ancestry(name="human")
tiefling = schema.Ancestry(name="tiefling")
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="intelligence", relative_value=1))
tiefling.add_modifier(schema.Modifier(name="Ability Score Increase", target="charisma", relative_value=2))
human = schema.Ancestry("human")
tiefling = schema.Ancestry("tiefling")
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="intelligence", relative_value=1))
tiefling.add_modifier(schema.Modifier("Ability Score Increase", target="charisma", relative_value=2))
darkvision = schema.AncestryTrait(
name="Darkvision",
"Darkvision",
description=(
"You can see in dim light within 60 feet of you as if it were bright light, and in darkness as if it "
"were dim light. You cant discern color in darkness, only shades of gray."
),
)
darkvision.add_modifier(schema.Modifier(name="Darkvision", target="vision_in_darkness", absolute_value=120))
darkvision.add_modifier(schema.Modifier("Darkvision", target="vision_in_darkness", absolute_value=120))
tiefling.add_trait(darkvision)
# classes
fighter = schema.CharacterClass(name="fighter", hit_dice="1d10", hit_dice_stat="CON")
rogue = schema.CharacterClass(name="rogue", hit_dice="1d8", hit_dice_stat="DEX")
fighter = schema.CharacterClass("fighter", hit_dice="1d10", hit_dice_stat="CON")
rogue = schema.CharacterClass("rogue", hit_dice="1d8", hit_dice_stat="DEX")
# characters
sabetha = schema.Character(name="Sabetha", ancestry=tiefling)
sabetha = schema.Character("Sabetha", ancestry=tiefling)
sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3)
bob = schema.Character(name="Bob", ancestry=human)
bob = schema.Character("Bob", ancestry=human)
# persist all the records we've created
db.add_or_update([sabetha, bob])

View File

@ -6,8 +6,7 @@ from contextlib import contextmanager
from functools import cached_property
import transaction
from pyramid_sqlalchemy import Session, init_sqlalchemy
from pyramid_sqlalchemy import metadata as _metadata
from pyramid_sqlalchemy.meta import Session
from sqlalchemy import create_engine
import ttfrog.db.schema
@ -43,7 +42,7 @@ class SQLDatabaseManager:
@cached_property
def metadata(self):
return _metadata
return ttfrog.db.schema.BaseObject.metadata
@cached_property
def tables(self):
@ -77,7 +76,8 @@ class SQLDatabaseManager:
return base64.urlsafe_b64encode(sha1bytes.digest()).decode("ascii")[:10]
def init(self):
init_sqlalchemy(self.engine)
self.session.configure(bind=self.engine, autoflush=False)
self.metadata.bind = self.engine
self.metadata.create_all(self.engine)
def dump(self, names: list = []):

View File

@ -1,8 +1,9 @@
from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueConstraint
from sqlalchemy import ForeignKey, Text, UniqueConstraint
from sqlalchemy.ext.associationproxy import association_proxy
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SizesEnum, SkillsMixin, SlugMixin
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, SlugMixin
from ttfrog.db.schema.classes import CharacterClass, ClassAttribute
from ttfrog.db.schema.modifiers import Modifier, ModifierMixin
__all__ = [
@ -31,11 +32,11 @@ def attr_map_creator(fields):
class AncestryTraitMap(BaseObject):
__tablename__ = "trait_map"
__table_args__ = (UniqueConstraint("ancestry_id", "ancestry_trait_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"))
ancestry_trait_id = Column(Integer, ForeignKey("ancestry_trait.id"))
trait = relationship("AncestryTrait", uselist=False, lazy="immediate")
level = Column(Integer, nullable=False, info={"min": 1, "max": 20})
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"))
ancestry_trait_id: Mapped[int] = mapped_column(ForeignKey("ancestry_trait.id"), init=False)
trait: Mapped["AncestryTrait"] = relationship(uselist=False, lazy="immediate")
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20})
class Ancestry(BaseObject, ModifierMixin):
@ -44,15 +45,20 @@ class Ancestry(BaseObject, ModifierMixin):
"""
__tablename__ = "ancestry"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, index=True, unique=True)
creature_type = Column(Enum(CreatureTypesEnum), nullable=False, default="humanoid")
size = Column(Enum(SizesEnum), nullable=False, default="Medium")
walk_speed = Column(Integer, nullable=False, default=30, info={"min": 0, "max": 99})
_fly_speed = Column(Integer, info={"min": 0, "max": 99})
_climb_speed = Column(Integer, info={"min": 0, "max": 99})
_swim_speed = Column(Integer, info={"min": 0, "max": 99})
_traits = relationship("AncestryTraitMap", cascade="all,delete,delete-orphan", lazy="immediate")
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(unique=True, nullable=False)
creature_type: Mapped[str] = mapped_column(nullable=False, default="humanoid")
size: Mapped[str] = mapped_column(nullable=False, default="medium")
walk_speed: Mapped[int] = mapped_column(nullable=False, default=30, info={"min": 0, "max": 99})
_fly_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_climb_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_swim_speed: Mapped[int] = mapped_column(init=False, nullable=True, info={"min": 0, "max": 99})
_traits = relationship(
"AncestryTraitMap", init=False, uselist=True, cascade="all,delete,delete-orphan", lazy="immediate"
)
@property
def traits(self):
@ -71,8 +77,12 @@ class Ancestry(BaseObject, ModifierMixin):
return self._swim_speed or int(self.speed / 2)
def add_trait(self, trait, level=1):
if trait not in self._traits:
self._traits.append(AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level))
if not self._traits or trait not in self._traits:
mapping = AncestryTraitMap(ancestry_id=self.id, trait=trait, level=level)
if not self._traits:
self._traits = [mapping]
else:
self._traits.append(mapping)
return True
return False
@ -86,9 +96,9 @@ class AncestryTrait(BaseObject, ModifierMixin):
"""
__tablename__ = "ancestry_trait"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
description = Column(Text)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
description: Mapped[Text] = mapped_column(Text, default="")
def __repr__(self):
return self.name
@ -97,13 +107,14 @@ class AncestryTrait(BaseObject, ModifierMixin):
class CharacterClassMap(BaseObject):
__tablename__ = "class_map"
__table_args__ = (UniqueConstraint("character_id", "character_class_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
character_class_id = Column(Integer, ForeignKey("character_class.id"), nullable=False)
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character: Mapped["Character"] = relationship(uselist=False, viewonly=True)
character_class: Mapped["CharacterClass"] = relationship(lazy="immediate")
character_class = relationship("CharacterClass", lazy="immediate")
character = relationship("Character", uselist=False, viewonly=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), init=False, nullable=False)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), init=False, nullable=False)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
def __repr__(self):
return "{self.character.name}, {self.character_class.name}, level {self.level}"
@ -112,12 +123,12 @@ class CharacterClassMap(BaseObject):
class CharacterClassAttributeMap(BaseObject):
__tablename__ = "character_class_attribute_map"
__table_args__ = (UniqueConstraint("character_id", "class_attribute_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
character_id = Column(Integer, ForeignKey("character.id"), nullable=False)
class_attribute_id = Column(Integer, ForeignKey("class_attribute.id"), nullable=False)
option_id = Column(Integer, ForeignKey("class_attribute_option.id"), nullable=False)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
character_id: Mapped[int] = mapped_column(ForeignKey("character.id"), nullable=False)
class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
option_id: Mapped[int] = mapped_column(ForeignKey("class_attribute_option.id"), nullable=False)
class_attribute = relationship("ClassAttribute", lazy="immediate")
class_attribute: Mapped["ClassAttribute"] = relationship(lazy="immediate")
option = relationship("ClassAttributeOption", lazy="immediate")
character_class = relationship(
@ -132,22 +143,23 @@ class CharacterClassAttributeMap(BaseObject):
class Character(BaseObject, ModifierMixin, SlugMixin, 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=10, nullable=False, info={"min": 0, "max": 999})
temp_hit_points = Column(Integer, default=0, nullable=False, info={"min": 0, "max": 999})
strength = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
dexterity = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
constitution = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
intelligence = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
wisdom = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
charisma = Column(Integer, nullable=False, default=10, info={"min": 0, "max": 30})
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
_vision = Column(Integer, info={"min": 0})
name: Mapped[str] = mapped_column(default="New Character", nullable=False)
armor_class: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 1, "max": 99})
hit_points: Mapped[int] = mapped_column(default=1, nullable=False, info={"min": 0, "max": 999})
max_hit_points: Mapped[int] = mapped_column(default=10, nullable=False, info={"min": 0, "max": 999})
temp_hit_points: Mapped[int] = mapped_column(default=0, nullable=False, info={"min": 0, "max": 999})
strength: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
dexterity: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
constitution: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
intelligence: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
wisdom: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
charisma: Mapped[int] = mapped_column(nullable=False, default=10, info={"min": 0, "max": 30})
proficiencies = Column(String)
_vision: Mapped[int] = mapped_column(default=None, nullable=True, info={"min": 0})
proficiencies: Mapped[str] = mapped_column(nullable=False, default="")
class_map = relationship("CharacterClassMap", cascade="all,delete,delete-orphan")
class_list = association_proxy("class_map", "id", creator=class_map_creator)
@ -155,8 +167,8 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
character_class_attribute_map = relationship("CharacterClassAttributeMap", cascade="all,delete,delete-orphan")
attribute_list = association_proxy("character_class_attribute_map", "id", creator=attr_map_creator)
ancestry_id = Column(Integer, ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry = relationship("Ancestry", uselist=False)
ancestry_id: Mapped[int] = mapped_column(ForeignKey("ancestry.id"), nullable=False, default="1")
ancestry: Mapped["Ancestry"] = relationship(uselist=False, default=None)
@property
def modifiers(self):
@ -255,7 +267,7 @@ class Character(BaseObject, ModifierMixin, SlugMixin, SavingThrowsMixin, SkillsM
level_in_class = level_in_class[0]
level_in_class.level = level
else:
self.class_list.append(CharacterClassMap(character_id=self.id, character_class=newclass, level=level))
self.class_list.append(CharacterClassMap(character=self, character_class=newclass, level=level))
for lvl in range(1, level + 1):
if not newclass.attributes_by_level[lvl]:
continue

View File

@ -1,9 +1,9 @@
from collections import defaultdict
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
from sqlalchemy.orm import relationship
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, StatsEnum
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin
__all__ = [
"ClassAttributeMap",
@ -15,16 +15,16 @@ __all__ = [
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)
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
class_attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), primary_key=True)
character_class_id: Mapped[int] = mapped_column(ForeignKey("character_class.id"), primary_key=True)
level: Mapped[int] = mapped_column(nullable=False, info={"min": 1, "max": 20}, default=1)
attribute = relationship("ClassAttribute", uselist=False, viewonly=True, lazy="immediate")
class ClassAttribute(BaseObject):
__tablename__ = "class_attribute"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
options = relationship("ClassAttributeOption", cascade="all,delete,delete-orphan", lazy="immediate")
def __repr__(self):
@ -33,18 +33,18 @@ class ClassAttribute(BaseObject):
class ClassAttributeOption(BaseObject):
__tablename__ = "class_attribute_option"
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
attribute_id = Column(Integer, ForeignKey("class_attribute.id"), nullable=False)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
attribute_id: Mapped[int] = mapped_column(ForeignKey("class_attribute.id"), nullable=False)
class CharacterClass(BaseObject, 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)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(index=True, unique=True)
hit_dice: Mapped[str] = mapped_column(default="1d6")
hit_dice_stat: Mapped[str] = mapped_column(default="")
proficiencies: Mapped[str] = mapped_column(default="")
attributes = relationship("ClassAttributeMap", cascade="all,delete,delete-orphan", lazy="immediate")
@property

View File

@ -1,8 +1,8 @@
from collections import defaultdict
from sqlalchemy import Column, Float, ForeignKey, Integer, String, UniqueConstraint
from sqlalchemy import ForeignKey, UniqueConstraint
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.orm import relationship
from sqlalchemy.orm import Mapped, mapped_column, relationship
from ttfrog.db.base import BaseObject
@ -14,12 +14,12 @@ class ModifierMap(BaseObject):
__tablename__ = "modifier_map"
__table_args__ = (UniqueConstraint("primary_table_name", "primary_table_id", "modifier_id"),)
id = Column(Integer, primary_key=True, autoincrement=True)
modifier_id = Column(Integer, ForeignKey("modifier.id"), nullable=False)
modifier = relationship("Modifier", uselist=False, lazy="immediate")
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
modifier_id: Mapped[int] = mapped_column(ForeignKey("modifier.id"), init=False)
modifier: Mapped["Modifier"] = relationship(uselist=False, lazy="immediate")
primary_table_name = Column(String, nullable=False)
primary_table_id = Column(Integer, nullable=False)
primary_table_name: Mapped[str] = mapped_column(nullable=False)
primary_table_id: Mapped[int] = mapped_column(nullable=False)
class Modifier(BaseObject):
@ -31,14 +31,14 @@ class Modifier(BaseObject):
"""
__tablename__ = "modifier"
id = Column(Integer, primary_key=True, autoincrement=True)
target = Column(String, nullable=False)
absolute_value = Column(Integer)
relative_value = Column(Integer)
multiply_value = Column(Float)
new_value = Column(String)
name = Column(String, nullable=False)
description = Column(String)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
target: Mapped[str] = mapped_column(nullable=False)
absolute_value: Mapped[int] = mapped_column(nullable=True, default=None)
relative_value: Mapped[int] = mapped_column(nullable=True, default=None)
multiply_value: Mapped[float] = mapped_column(nullable=True, default=None)
new_value: Mapped[str] = mapped_column(nullable=True, default=None)
description: Mapped[str] = mapped_column(default="")
class ModifierMixin:
@ -56,14 +56,16 @@ class ModifierMixin:
Example:
>>> class Item(BaseObject, ModifierMixin):
id = Column(Integer, primary_key=True, autoincrement=True)
name = Column(String, nullable=False)
id: Mapped[int] = mapped_column(init=False, primary_key=True, autoincrement=True)
name: Mapped[str] = mapped_column(nullable=False)
>>> dwarven_belt = Item(name="Dwarven Belt")
>>> dwarven_belt.add_modifier(Modifier(name="STR+1", target="strength", relative_value=1))
>>> dwarven_belt.modifiers
{'strength': [Modifier(id=1, target='strength', name='STR+1', relative_value=1 ... ]}
"""
_modifiable_attributes = dict()
@declared_attr
def modifier_map(cls):
return relationship(
@ -104,7 +106,36 @@ class ModifierMixin:
return True
def remove_modifier(self, modifier):
if modifier.id not in [mod.modifier_id for mod in self.modifier_map]:
if modifier not in self.modifiers[modifier.target]:
return False
self.modifier_map = [mapping for mapping in self.modifier_map if mapping.modifier != modifier]
return True
def apply_modifiers(self, target, initial):
if not self._modifiable_attributes:
raise NotImplementedError(
f"You must define the '_modifiable_attributes' property on {self.__class__.__name__}."
)
modifiers = list(reversed(self.modifiers.get(target, [])))
if initial is None:
return initial
if isinstance(initial, int):
absolute = [mod for mod in modifiers if mod.absolute_value is not None]
if absolute:
return absolute[0].absolute_value
multiple = [mod for mod in modifiers if mod.multiply_value is not None]
if multiple:
return int(initial * multiple[0].multiply_value + 0.5)
return initial + sum(mod.relative_value for mod in modifiers if mod.relative_value is not None)
new = [mod for mod in modifiers if mod.new_value is not None]
if new:
return new[0].new_value
return initial
def __getattr__(self, attr_name):
prop = self._modifiable_attributes.get(attr_name, None)
if not prop:
raise AttributeError(f"Attribute not found: {attr_name}")
return self.apply_modifiers(attr_name, getattr(self, prop))