Add json import/export

This commit is contained in:
evilchili 2024-06-30 23:21:23 -07:00
parent 68251ff4e9
commit 551140b5bc
9 changed files with 334 additions and 49 deletions

View File

@ -80,11 +80,11 @@ def serve(
""" """
# delay loading the app until we have configured our environment # delay loading the app until we have configured our environment
from ttfrog.db.bootstrap import bootstrap from ttfrog.db.bootstrap import loader
from ttfrog.webserver import application from ttfrog.webserver import application
print("Starting TableTop Frog server...") print("Starting TableTop Frog server...")
bootstrap() loader.load()
application.start(host=host, port=port, debug=debug) application.start(host=host, port=port, debug=debug)
@ -93,13 +93,13 @@ def setup(context: typer.Context):
""" """
(Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration. (Re)Initialize TableTop Frog. Idempotent; will preserve any existing configuration.
""" """
from ttfrog.db.bootstrap import bootstrap from ttfrog.db.bootstrap import loader
if not os.path.exists(app_state["env"]): if not os.path.exists(app_state["env"]):
app_state["env"].parent.mkdir(parents=True, exist_ok=True) app_state["env"].parent.mkdir(parents=True, exist_ok=True)
app_state["env"].write_text(dedent(SETUP_HELP)) app_state["env"].write_text(dedent(SETUP_HELP))
print(f"Wrote defaults file {app_state['env']}.") print(f"Wrote defaults file {app_state['env']}.")
bootstrap() loader.load()
@db_app.command() @db_app.command()

View File

@ -1,36 +0,0 @@
from ttfrog.db import schema
from ttfrog.db.manager import db
def bootstrap():
db.metadata.drop_all(bind=db.engine)
db.init()
with db.transaction():
# ancestries
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(
"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("Darkvision", target="vision_in_darkness", absolute_value=120))
tiefling.add_trait(darkvision)
# classes
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("Sabetha", ancestry=tiefling, _intelligence=14)
sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3)
bob = schema.Character("Bob", ancestry=human)
# persist all the records we've created
db.add_or_update([sabetha, bob])

View File

View File

@ -0,0 +1,208 @@
{
"ancestry": [
{
"id": 1,
"name": "human",
"creature_type": "humanoid",
"size": "medium",
"speed": 30,
"_fly_speed": null,
"_climb_speed": null,
"_swim_speed": null
},
{
"id": 2,
"name": "tiefling",
"creature_type": "humanoid",
"size": "medium",
"speed": 30,
"_fly_speed": null,
"_climb_speed": null,
"_swim_speed": null
}
],
"ancestry_trait": [
{
"id": 1,
"name": "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 can\u2019t discern color in darkness, only shades of gray."
}
],
"character_class": [
{
"id": 1,
"name": "fighter",
"hit_die_name": "1d10",
"hit_die_stat_name": "constitution",
"starting_skills": 0
},
{
"id": 2,
"name": "rogue",
"hit_die_name": "1d8",
"hit_die_stat_name": "dexterity",
"starting_skills": 0
}
],
"class_attribute": [],
"modifier": [
{
"id": 1,
"name": "Darkvision",
"target": "vision_in_darkness",
"stacks": false,
"absolute_value": 120,
"multiply_value": null,
"multiply_attribute": null,
"relative_value": null,
"relative_attribute": null,
"new_value": null,
"description": ""
},
{
"id": 2,
"name": "Ability Score Increase",
"target": "intelligence",
"stacks": false,
"absolute_value": null,
"multiply_value": null,
"multiply_attribute": null,
"relative_value": 1,
"relative_attribute": null,
"new_value": null,
"description": ""
},
{
"id": 3,
"name": "Ability Score Increase",
"target": "charisma",
"stacks": false,
"absolute_value": null,
"multiply_value": null,
"multiply_attribute": null,
"relative_value": 2,
"relative_attribute": null,
"new_value": null,
"description": ""
}
],
"skill": [],
"transaction_log": [],
"character": [
{
"id": 1,
"name": "Sabetha",
"hit_points": 10,
"temp_hit_points": 0,
"_max_hit_points": 10,
"_armor_class": 10,
"_strength": 10,
"_dexterity": 10,
"_constitution": 10,
"_intelligence": 14,
"_wisdom": 10,
"_charisma": 10,
"_vision": null,
"exhaustion": 0,
"ancestry_id": 2,
"slug": "nMnWu"
},
{
"id": 2,
"name": "Bob",
"hit_points": 10,
"temp_hit_points": 0,
"_max_hit_points": 10,
"_armor_class": 10,
"_strength": 10,
"_dexterity": 10,
"_constitution": 10,
"_intelligence": 10,
"_wisdom": 10,
"_charisma": 10,
"_vision": null,
"exhaustion": 0,
"ancestry_id": 1,
"slug": "PjPdM"
}
],
"class_attribute_map": [],
"class_attribute_option": [],
"class_skill_map": [],
"modifier_map": [
{
"id": 1,
"modifier_id": 1,
"primary_table_name": "ancestry_trait",
"primary_table_id": 1
},
{
"id": 2,
"modifier_id": 2,
"primary_table_name": "ancestry",
"primary_table_id": 2
},
{
"id": 3,
"modifier_id": 3,
"primary_table_name": "ancestry",
"primary_table_id": 2
}
],
"trait_map": [
{
"id": 1,
"ancestry_id": 2,
"ancestry_trait_id": 1,
"level": 1
}
],
"character_class_attribute_map": [],
"character_skill_map": [],
"class_map": [
{
"id": 1,
"character_id": 1,
"character_class_id": 1,
"level": 2
},
{
"id": 2,
"character_id": 1,
"character_class_id": 2,
"level": 3
}
],
"hit_die": [
{
"id": 1,
"character_id": 1,
"character_class_id": 1,
"spent": false
},
{
"id": 2,
"character_id": 1,
"character_class_id": 1,
"spent": false
},
{
"id": 3,
"character_id": 1,
"character_class_id": 2,
"spent": false
},
{
"id": 4,
"character_id": 1,
"character_class_id": 2,
"spent": false
},
{
"id": 5,
"character_id": 1,
"character_class_id": 2,
"spent": false
}
]
}

View File

@ -0,0 +1,29 @@
import json
from pathlib import Path
from ttfrog.db import schema
from ttfrog.db.manager import db
DATA_PATH = Path(__file__).parent
def load(data: str = ""):
db.metadata.drop_all(bind=db.engine)
db.init()
with db.transaction():
if not data:
data = (DATA_PATH / "bootstrap.json").read_text()
db.load(json.loads(data))
tiefling = db.Ancestry.filter_by(name="tiefling").one()
human = db.Ancestry.filter_by(name="human").one()
fighter = db.CharacterClass.filter_by(name="fighter").one()
rogue = db.CharacterClass.filter_by(name="rogue").one()
sabetha = schema.Character("Sabetha", ancestry=tiefling, _intelligence=14)
sabetha.add_class(fighter, level=2)
sabetha.add_class(rogue, level=3)
bob = schema.Character("Bob", ancestry=human)
# persist all the records we've created
db.add_or_update([sabetha, bob])

View File

@ -4,16 +4,15 @@ import json
import os import os
from contextlib import contextmanager from contextlib import contextmanager
from functools import cached_property from functools import cached_property
from sqlite3 import IntegrityError
import transaction import transaction
from pyramid_sqlalchemy.meta import Session from pyramid_sqlalchemy.meta import Session
from sqlalchemy import create_engine, event from sqlalchemy import create_engine, event, insert
import ttfrog.db.schema from ttfrog.db import schema
from ttfrog.path import database from ttfrog.path import database
assert ttfrog.db.schema
class AlchemyEncoder(json.JSONEncoder): class AlchemyEncoder(json.JSONEncoder):
def default(self, obj): def default(self, obj):
@ -42,7 +41,7 @@ class SQLDatabaseManager:
@cached_property @cached_property
def metadata(self): def metadata(self):
return ttfrog.db.schema.BaseObject.metadata return schema.BaseObject.metadata
@cached_property @cached_property
def tables(self): def tables(self):
@ -87,8 +86,18 @@ class SQLDatabaseManager:
results[table_name] = [dict(row._mapping) for row in self.query(table).all()] results[table_name] = [dict(row._mapping) for row in self.query(table).all()]
return json.dumps(results, indent=2, cls=AlchemyEncoder) return json.dumps(results, indent=2, cls=AlchemyEncoder)
def load(self, data: dict):
for table_name, rows in data.items():
table = self.tables.get(table_name, None)
if table is None:
raise IntegrityError(f"Table {table_name} not found in database.")
if not rows:
continue
query = insert(table), rows
self.session.execute(*query)
def __getattr__(self, name: str): def __getattr__(self, name: str):
return self.query(getattr(ttfrog.db.schema, name)) return self.query(getattr(schema, name))
db = SQLDatabaseManager() db = SQLDatabaseManager()

View File

@ -391,7 +391,7 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
self.add_skill(skill, proficient=True, character_class=newclass) self.add_skill(skill, proficient=True, character_class=newclass)
# add hit dice # add hit dice
existing = len(self.hit_dice[newclass.name]) existing = len([die for die in self._hit_dice if die.character_class_id == newclass.id])
for lvl in range(level - existing): for lvl in range(level - existing):
self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id)) self._hit_dice.append(HitDie(character_id=self.id, character_class_id=newclass.id))
@ -431,8 +431,8 @@ class Character(BaseObject, SlugMixin, ModifierMixin):
return True return True
def add_skill(self, skill, proficient=False, expert=False, character_class=None): def add_skill(self, skill, proficient=False, expert=False, character_class=None):
if not self.id: # if not self.id:
raise Exception("Cannot add a skill before the character has been persisted.") # raise Exception("Cannot add a skill before the character has been persisted.")
skillmap = None skillmap = None
exists = False exists = False
if skill in self.skills: if skill in self.skills:

View File

@ -0,0 +1,52 @@
from enum import StrEnum, auto
class Conditions(StrEnum):
blinded = auto()
charmed = auto()
deafened = auto()
frightened = auto()
grappled = auto()
incapacitated = auto()
invisible = auto()
paralyzed = auto()
petrified = auto()
poisoned = auto()
prone = auto()
restrained = auto()
stunned = auto()
unconscious = auto()
dead = auto()
class DamageType(StrEnum):
piercing = auto()
slashing = auto()
bludgeoning = auto()
fire = auto()
cold = auto()
lightning = auto()
thunder = auto()
acid = auto()
poison = auto()
radiant = auto()
necrotic = auto()
psychic = auto()
force = auto()
magical = auto()
magical_piercing = auto()
magical_slashing = auto()
magical_bludgeoning = auto()
silvered_piercing = auto()
silvered_slashing = auto()
silvered_bludgeoning = auto()
adamantium_piercing = auto()
adamantium_slashing = auto()
adamantium_bludgeoning = auto()
class Defenses(StrEnum):
vulnerable = auto()
resistant = auto()
immune = auto()
absorbs = auto()

23
test/test_bootstrap.py Normal file
View File

@ -0,0 +1,23 @@
import json
from ttfrog.db.bootstrap import loader
def test_dump_load(db, bootstrap):
# dump the bootstrapped data
data = json.loads(db.dump())
# clear the database and reinitialize
db.metadata.drop_all(bind=db.engine)
db.init()
# load the dump
db.load(data)
# dump again and compare to the initial data
assert data == json.loads(db.dump())
def test_loader(db, bootstrap):
loader.load(db.dump())
assert len(db.Ancestry.all()) > 0