Add json import/export
This commit is contained in:
parent
68251ff4e9
commit
551140b5bc
|
@ -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()
|
||||||
|
|
|
@ -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 can’t 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])
|
|
0
src/ttfrog/db/bootstrap/__init__.py
Normal file
0
src/ttfrog/db/bootstrap/__init__.py
Normal file
208
src/ttfrog/db/bootstrap/bootstrap.json
Normal file
208
src/ttfrog/db/bootstrap/bootstrap.json
Normal 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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
29
src/ttfrog/db/bootstrap/loader.py
Normal file
29
src/ttfrog/db/bootstrap/loader.py
Normal 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])
|
|
@ -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()
|
||||||
|
|
|
@ -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:
|
||||||
|
|
52
src/ttfrog/db/schema/constants.py
Normal file
52
src/ttfrog/db/schema/constants.py
Normal 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
23
test/test_bootstrap.py
Normal 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
|
Loading…
Reference in New Issue
Block a user