Make objects iterable by default, add tests, refactoring
This commit is contained in:
+33
-14
@@ -36,11 +36,14 @@ HOST={default_host}
|
||||
PORT={default_port}
|
||||
"""
|
||||
|
||||
db_app = typer.Typer()
|
||||
app = typer.Typer()
|
||||
app.add_typer(db_app, name="db", help="Manage the database.")
|
||||
app_state = dict()
|
||||
|
||||
|
||||
@app.callback()
|
||||
@db_app.callback()
|
||||
def main(
|
||||
context: typer.Context,
|
||||
root: Optional[Path] = typer.Option(
|
||||
@@ -59,20 +62,6 @@ def main(
|
||||
)
|
||||
|
||||
|
||||
@app.command()
|
||||
def setup(context: typer.Context):
|
||||
"""
|
||||
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
||||
"""
|
||||
from ttfrog.db.bootstrap import bootstrap
|
||||
|
||||
if not os.path.exists(app_state["env"]):
|
||||
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
||||
app_state["env"].write_text(dedent(SETUP_HELP))
|
||||
print(f"Wrote defaults file {app_state['env']}.")
|
||||
bootstrap()
|
||||
|
||||
|
||||
@app.command()
|
||||
def serve(
|
||||
context: typer.Context,
|
||||
@@ -99,5 +88,35 @@ def serve(
|
||||
application.start(host=host, port=port, debug=debug)
|
||||
|
||||
|
||||
@db_app.command()
|
||||
def setup(context: typer.Context):
|
||||
"""
|
||||
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
|
||||
"""
|
||||
from ttfrog.db.bootstrap import bootstrap
|
||||
|
||||
if not os.path.exists(app_state["env"]):
|
||||
app_state["env"].parent.mkdir(parents=True, exist_ok=True)
|
||||
app_state["env"].write_text(dedent(SETUP_HELP))
|
||||
print(f"Wrote defaults file {app_state['env']}.")
|
||||
bootstrap()
|
||||
|
||||
|
||||
@db_app.command()
|
||||
def list(context: typer.Context):
|
||||
from ttfrog.db.manager import db
|
||||
print("\n".join(sorted(db.tables.keys())))
|
||||
|
||||
|
||||
@db_app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True})
|
||||
def dump(context: typer.Context):
|
||||
"""
|
||||
Dump tables (or the entire database) as a JSON blob.
|
||||
"""
|
||||
from ttfrog.db.manager import db
|
||||
db.init()
|
||||
print(db.dump(context.args))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
app()
|
||||
|
||||
+9
-14
@@ -2,7 +2,7 @@ import enum
|
||||
|
||||
import nanoid
|
||||
from nanoid_dictionary import human_alphabet
|
||||
from pyramid_sqlalchemy import BaseObject
|
||||
from pyramid_sqlalchemy import BaseObject as _BaseObject
|
||||
from slugify import slugify
|
||||
from sqlalchemy import Column, String
|
||||
|
||||
@@ -19,10 +19,11 @@ class SlugMixin:
|
||||
return "-".join([self.slug, slugify(self.name.title().replace(" ", ""), ok="", only_ascii=True, lower=False)])
|
||||
|
||||
|
||||
class IterableMixin:
|
||||
class BaseObject(_BaseObject):
|
||||
"""
|
||||
Allows for iterating over Model objects' column names and values
|
||||
"""
|
||||
__abstract__ = True
|
||||
|
||||
def __iter__(self):
|
||||
values = vars(self)
|
||||
@@ -42,14 +43,11 @@ class IterableMixin:
|
||||
relvals.append(rel)
|
||||
yield relname, relvals
|
||||
|
||||
def __json__(self, request):
|
||||
serialized = dict()
|
||||
for key, value in self:
|
||||
try:
|
||||
serialized[key] = getattr(self.value, "__json__")(request)
|
||||
except AttributeError:
|
||||
serialized[key] = value
|
||||
return serialized
|
||||
def __json__(self):
|
||||
"""
|
||||
Provide a custom JSON encoder.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self))
|
||||
@@ -90,7 +88,7 @@ class EnumField(enum.Enum):
|
||||
A serializable enum.
|
||||
"""
|
||||
|
||||
def __json__(self, request):
|
||||
def __json__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
@@ -116,6 +114,3 @@ CREATURE_TYPES = [
|
||||
]
|
||||
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]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from functools import cached_property
|
||||
@@ -9,13 +10,19 @@ from pyramid_sqlalchemy import Session, init_sqlalchemy
|
||||
from pyramid_sqlalchemy import metadata as _metadata
|
||||
from sqlalchemy import create_engine
|
||||
|
||||
|
||||
import ttfrog.db.schema
|
||||
from ttfrog.path import database
|
||||
|
||||
# from sqlalchemy.exc import IntegrityError
|
||||
assert ttfrog.db.schema
|
||||
|
||||
|
||||
ttfrog.db.schema
|
||||
class AlchemyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
try:
|
||||
return getattr(obj, '__json__')()
|
||||
except (AttributeError, NotImplementedError): # pragma: no cover
|
||||
return super().default(obj)
|
||||
|
||||
|
||||
class SQLDatabaseManager:
|
||||
@@ -49,11 +56,11 @@ class SQLDatabaseManager:
|
||||
yield tm
|
||||
try:
|
||||
tm.commit()
|
||||
except Exception:
|
||||
except Exception: # pragam: no cover
|
||||
tm.abort()
|
||||
raise
|
||||
|
||||
def add(self, *args, **kwargs):
|
||||
def add_or_update(self, *args, **kwargs):
|
||||
self.session.add(*args, **kwargs)
|
||||
self.session.flush()
|
||||
|
||||
@@ -71,11 +78,12 @@ class SQLDatabaseManager:
|
||||
init_sqlalchemy(self.engine)
|
||||
self.metadata.create_all(self.engine)
|
||||
|
||||
def dump(self):
|
||||
def dump(self, names: list = []):
|
||||
results = {}
|
||||
for table_name, table in self.tables.items():
|
||||
results[table_name] = [row for row in self.query(table).all()]
|
||||
return results
|
||||
if not names or table_name in names:
|
||||
results[table_name] = [dict(row._mapping) for row in self.query(table).all()]
|
||||
return json.dumps(results, indent=2, cls=AlchemyEncoder)
|
||||
|
||||
def __getattr__(self, name: str):
|
||||
try:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from .character import *
|
||||
from .classes import *
|
||||
from .property import *
|
||||
from .transaction import *
|
||||
from .log import *
|
||||
|
||||
@@ -2,7 +2,7 @@ from sqlalchemy import Column, Enum, ForeignKey, Integer, String, Text, UniqueCo
|
||||
from sqlalchemy.ext.associationproxy import association_proxy
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from ttfrog.db.base import BaseObject, Bases, CreatureTypesEnum, IterableMixin, SavingThrowsMixin, SkillsMixin
|
||||
from ttfrog.db.base import BaseObject, CreatureTypesEnum, SavingThrowsMixin, SkillsMixin, SlugMixin
|
||||
|
||||
__all__ = [
|
||||
"Ancestry",
|
||||
@@ -28,6 +28,7 @@ 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"))
|
||||
@@ -35,7 +36,7 @@ class AncestryTraitMap(BaseObject):
|
||||
level = Column(Integer, nullable=False, info={"min": 1, "max": 20})
|
||||
|
||||
|
||||
class Ancestry(*Bases):
|
||||
class Ancestry(BaseObject):
|
||||
"""
|
||||
A character ancestry ("race"), which has zero or more AncestryTraits.
|
||||
"""
|
||||
@@ -44,13 +45,13 @@ class Ancestry(*Bases):
|
||||
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")
|
||||
_traits = relationship("AncestryTraitMap", lazy="immediate")
|
||||
|
||||
def __repr__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class AncestryTrait(BaseObject, IterableMixin):
|
||||
class AncestryTrait(BaseObject):
|
||||
"""
|
||||
A trait granted to a character via its Ancestry.
|
||||
"""
|
||||
@@ -64,12 +65,12 @@ class AncestryTrait(BaseObject, IterableMixin):
|
||||
return self.name
|
||||
|
||||
|
||||
class CharacterClassMap(BaseObject, IterableMixin):
|
||||
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)
|
||||
mapping = UniqueConstraint(character_id, character_class_id)
|
||||
level = Column(Integer, nullable=False, info={"min": 1, "max": 20}, default=1)
|
||||
|
||||
character_class = relationship("CharacterClass", lazy="immediate")
|
||||
@@ -79,13 +80,13 @@ class CharacterClassMap(BaseObject, IterableMixin):
|
||||
return "{self.character.name}, {self.character_class.name}, level {self.level}"
|
||||
|
||||
|
||||
class CharacterClassAttributeMap(BaseObject, IterableMixin):
|
||||
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)
|
||||
mapping = UniqueConstraint(character_id, class_attribute_id)
|
||||
|
||||
class_attribute = relationship("ClassAttribute", lazy="immediate")
|
||||
option = relationship("ClassAttributeOption", lazy="immediate")
|
||||
@@ -100,7 +101,7 @@ class CharacterClassAttributeMap(BaseObject, IterableMixin):
|
||||
)
|
||||
|
||||
|
||||
class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
class Character(BaseObject, SlugMixin, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, default="New Character", nullable=False)
|
||||
@@ -132,7 +133,7 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
|
||||
@property
|
||||
def traits(self):
|
||||
return [mapping.trait for mapping in self.ancestry.traits]
|
||||
return [mapping.trait for mapping in self.ancestry._traits]
|
||||
|
||||
@property
|
||||
def level(self):
|
||||
@@ -172,8 +173,11 @@ class Character(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
|
||||
def add_class_attribute(self, attribute, option):
|
||||
for thisclass in self.classes.values():
|
||||
# this test is failing?
|
||||
if attribute.name in thisclass.attributes_by_level.get(self.levels[thisclass.name], {}):
|
||||
current_level = self.levels[thisclass.name]
|
||||
current_attributes = thisclass.attributes_by_level.get(current_level, {})
|
||||
if attribute.name in current_attributes:
|
||||
if attribute.name in self.class_attributes:
|
||||
return True
|
||||
self.attribute_list.append(
|
||||
CharacterClassAttributeMap(
|
||||
character_id=self.id, class_attribute_id=attribute.id, option_id=option.id
|
||||
|
||||
@@ -3,7 +3,7 @@ from collections import defaultdict
|
||||
from sqlalchemy import Column, Enum, ForeignKey, Integer, String
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
from ttfrog.db.base import BaseObject, Bases, IterableMixin, SavingThrowsMixin, SkillsMixin, StatsEnum
|
||||
from ttfrog.db.base import BaseObject, SavingThrowsMixin, SkillsMixin, StatsEnum
|
||||
|
||||
__all__ = [
|
||||
"ClassAttributeMap",
|
||||
@@ -13,7 +13,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class ClassAttributeMap(BaseObject, IterableMixin):
|
||||
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)
|
||||
@@ -21,7 +21,7 @@ class ClassAttributeMap(BaseObject, IterableMixin):
|
||||
attribute = relationship("ClassAttribute", uselist=False, viewonly=True, lazy="immediate")
|
||||
|
||||
|
||||
class ClassAttribute(BaseObject, IterableMixin):
|
||||
class ClassAttribute(BaseObject):
|
||||
__tablename__ = "class_attribute"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, nullable=False)
|
||||
@@ -31,14 +31,14 @@ class ClassAttribute(BaseObject, IterableMixin):
|
||||
return f"{self.id}: {self.name}"
|
||||
|
||||
|
||||
class ClassAttributeOption(BaseObject, IterableMixin):
|
||||
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)
|
||||
|
||||
|
||||
class CharacterClass(*Bases, SavingThrowsMixin, SkillsMixin):
|
||||
class CharacterClass(BaseObject, SavingThrowsMixin, SkillsMixin):
|
||||
__tablename__ = "character_class"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from sqlalchemy import Column, Integer, String, Text
|
||||
|
||||
from ttfrog.db.base import BaseObject, IterableMixin
|
||||
from ttfrog.db.base import BaseObject
|
||||
|
||||
__all__ = ["TransactionLog"]
|
||||
|
||||
|
||||
class TransactionLog(BaseObject, IterableMixin):
|
||||
class TransactionLog(BaseObject):
|
||||
__tablename__ = "transaction_log"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
source_table_name = Column(String, index=True, nullable=False)
|
||||
@@ -1,6 +1,6 @@
|
||||
from sqlalchemy import Column, Integer, String, Text, UniqueConstraint
|
||||
|
||||
from ttfrog.db.base import BaseObject, Bases, IterableMixin
|
||||
from ttfrog.db.base import BaseObject
|
||||
|
||||
__all__ = [
|
||||
"Skill",
|
||||
@@ -9,7 +9,7 @@ __all__ = [
|
||||
]
|
||||
|
||||
|
||||
class Skill(*Bases):
|
||||
class Skill(BaseObject):
|
||||
__tablename__ = "skill"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
@@ -19,7 +19,7 @@ class Skill(*Bases):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Proficiency(*Bases):
|
||||
class Proficiency(BaseObject):
|
||||
__tablename__ = "proficiency"
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
name = Column(String, index=True, unique=True)
|
||||
@@ -28,7 +28,7 @@ class Proficiency(*Bases):
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Modifier(BaseObject, IterableMixin):
|
||||
class Modifier(BaseObject):
|
||||
__tablename__ = "modifier"
|
||||
__table_args__ = (UniqueConstraint("source_table_name", "source_table_id", "value", "type", "target"),)
|
||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||
|
||||
Reference in New Issue
Block a user