Repository: joshbarrass/SkyrimCrafting

Issue:
Add database wrapper functions
These functions should simplify the process of retrieving data. At this stage, these functions should be generic; there may be things we don't need at this stage. When it comes to implementing the functionality of the program, these functions should be the basis for interacting with the database, and it would be better to implement more complex functions that use the database on top of these functions; the amount of direct interaction with SQLAlchemy should be minimised.

Comments:

Patch:
diff --git a/app.py b/app.py
new file mode 100644
index 0000000..9f54825
--- /dev/null
+++ b/app.py
@@ -0,0 +1,1 @@
+from database.wrapper import Database
diff --git a/database/__init__.py b/database/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/database/database.py b/database/database.py
deleted file mode 100644
index 3e051af..0000000
--- a/database/database.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from sqlalchemy import create_engine
-from sqlalchemy.orm import sessionmaker
-
-from models import Item, PotionEffect, PotionItem, Recipe, CraftingIngredient
-
-class Database:
-    def __init__(self, url):
-        self.engine = create_engine(url)
-        Item.metadata.create_all(self.engine)
-        PotionEffect.metadata.create_all(self.engine)
-        PotionItem.metadata.create_all(self.engine)
-        Recipe.metadata.create_all(self.engine)
-        CraftingIngredient.metadata.create_all(self.engine)
-
-        self.SessionMaker = sessionmaker(bind=self.engine)
diff --git a/database/models.py b/database/models.py
index 05dab98..046ff7e 100644
--- a/database/models.py
+++ b/database/models.py
@@ -110,6 +110,9 @@ class Recipe(Base):
             "{table}.id".format(table=Requirement.__tablename__)
         ),
     )
+    requirement = relationship(
+        "Requirement", foreign_keys="Recipe.requirement_id"
+    )
     quantity = Column(Integer)
 
     def __repr__(self):
diff --git a/database/wrapper.py b/database/wrapper.py
new file mode 100644
index 0000000..a498efc
--- /dev/null
+++ b/database/wrapper.py
@@ -0,0 +1,216 @@
+from typing import List, Tuple, Union
+
+from sqlalchemy import create_engine
+from sqlalchemy.orm import sessionmaker
+
+from database.models import Item, PotionEffect, PotionItem, Requirement, Recipe, CraftingIngredient
+
+class Database:
+    def __init__(self, url):
+        self.engine = create_engine(url)
+        Item.metadata.create_all(self.engine)
+        PotionEffect.metadata.create_all(self.engine)
+        PotionItem.metadata.create_all(self.engine)
+        Recipe.metadata.create_all(self.engine)
+        CraftingIngredient.metadata.create_all(self.engine)
+
+        self.NewSession = sessionmaker(bind=self.engine)
+        self.session = None
+
+    def new_session(self, commit=False):
+        if self.session is not None and commit:
+            self.session.commit()
+        self.session = self.NewSession()
+
+    def have_session(self):
+        if self.session is None:
+            self.session = self.NewSession()
+
+    def commit(self):
+        return self.session.commit()
+
+    def rollback(self):
+        return self.session.rollback()
+
+    def close_session(self):
+        self.session.close()
+        self.session = None
+
+    def new_item(
+        self,
+        name: str,
+        value: int,
+        weight: float,
+        id: int = None
+    ) -> Item:
+        self.have_session()
+        newitem = Item(
+            name=name,
+            value=value,
+            weight=weight,
+        )
+
+        if id is not None:
+            newitem.id = id
+
+        self.session.add(newitem)
+        return newitem
+
+    def get_item_by_ID(self, ID: Union[int, str]) -> Item:
+        self.have_session()
+        if isinstance(ID, str):
+            ID = int(ID, 16)
+        return self.session.query(Item).filter_by(id=ID).first()
+
+    def get_items_by_name(self, name: str) -> List[Item]:
+        self.have_session()
+        return self.session.query(Item).filter_by(name=name).all()
+
+    def new_potion_effect(
+        self,
+        name: str,
+        desc: str,
+        cost: float,
+        mag: int,
+        dur: int,
+        value: int,
+        is_negative: bool,
+        vary_duration: bool,
+        id: int = None,
+    ) -> PotionEffect:
+        self.have_session()
+        neweffect = PotionEffect(
+            name=name,
+            is_negative=is_negative,
+            description=desc,
+            base_cost=cost,
+            base_mag=mag,
+            base_dur=dur,
+            value=value,
+            vary_duration=vary_duration,
+        )
+
+        if id is not None:
+            neweffect.id = id
+
+        self.session.add(neweffect)
+        return neweffect
+
+    def get_effect_by_ID(self, ID: int) -> PotionEffect:
+        self.have_session()
+        return self.session.query(PotionEffect).filter_by(id=ID
+                                                          ).first()
+
+    def get_effects_by_name(self, name: str) -> List[PotionEffect]:
+        self.have_session()
+        return self.session.query(PotionEffect).filter_by(name=name
+                                                          ).all()
+
+    def get_effects_by_item(self, item: Item) -> List[PotionItem]:
+        self.have_session()
+        assert item.id is not None
+        potionitems = self.session.query(PotionItem).filter_by(
+            item_id=item.id,
+        ).all()
+        return potionitems
+
+    def add_effect_to_ingredient(
+        self,
+        effect: PotionEffect,
+        item: PotionEffect,
+        mag_mult: float = 1,
+        dur_mult: float = 1,
+        val_mult: float = 1
+    ):
+        self.have_session()
+        assert item.id is not None
+        assert effect.id is not None
+        new = PotionItem(
+            item_id=item.id,
+            effect_id=effect.id,
+            mag_multiplier=mag_mult,
+            dur_multiplier=dur_mult,
+            val_multiplier=val_mult
+        )
+        return self.session.add(new)
+
+    def get_items_by_effect(self,
+                            effect: PotionEffect) -> List[PotionItem]:
+        self.have_session()
+        assert PotionEffect.id is not None
+        potion_items = self.session.query(PotionItem).filter_by(
+            effect_id=effect.id
+        ).all()
+        return potion_items
+
+    def add_requirement(self, name: str, desc: str) -> Requirement:
+        self.have_session()
+        requirement = Requirement(name=name, desc=desc)
+        self.session.add(requirement)
+        return requirement
+
+    def get_requirements_by_name(self, name: str) -> List[Requirement]:
+        self.have_session()
+        reqs = self.session.query(Requirement).filter_by(name=name
+                                                         ).all()
+        return reqs
+
+    def add_recipe(
+        self,
+        result: Item,
+        quantity: int,
+        requirement: Requirement = None
+    ) -> Recipe:
+        self.have_session()
+        assert result.id is not None
+        recipe = Recipe(result_id=result.id, quantity=quantity)
+
+        if requirement is not None:
+            assert requirement.id is not None
+            recipe.requirement_id = requirement.id
+
+        self.session.add(recipe)
+        return recipe
+
+    def add_ingredient_to_recipe(
+        self, recipe: Recipe, ingredient: Item, quantity: int = 1
+    ):
+        self.have_session()
+        assert recipe.id is not None
+        assert ingredient.id is not None
+        self.session.add(
+            CraftingIngredient(
+                recipe_id=recipe.id,
+                ingredient_id=ingredient.id,
+                quantity=quantity,
+            )
+        )
+
+    def get_ingredients_for_recipe(
+        self, recipe: Recipe
+    ) -> List[CraftingIngredient]:
+        self.have_session()
+        assert recipe.id is not None
+        crafting_ingredients = self.session.query(
+            CraftingIngredient
+        ).filter_by(
+            recipe_id=recipe.id,
+        ).all()
+        return crafting_ingredients
+
+    def get_recipes_for_item(
+        self, item: Item, requirements: List[Requirement] = None
+    ):
+        """Get all available Recipes that produce a given item. Requirements can be used to restrict the available recipes. A list of Requirements should be given as requirements. requirements=[] will list all recipes with no prerequisites, whilst requirements=None will list all recipes regardless of requirements."""
+        self.have_session()
+        assert item.id is not None
+        query = self.session.query(Recipe).filter_by(result_id=item.id)
+
+        if requirements is not None:
+            reqs = [None]
+            for req in requirements:
+                assert req.id is not None
+                reqs.append(req.id)
+            query.filter(Recipe.requirement_id.in_(reqs))
+
+        return query.all()


Test patch:
diff --git a/test/test_wrappers.py b/test/test_wrappers.py
new file mode 100644
index 0000000..d7f0cc2
--- /dev/null
+++ b/test/test_wrappers.py
@@ -0,0 +1,194 @@
+import unittest
+from sqlalchemy import MetaData
+from database.wrapper import Database
+from database.models import Item, PotionEffect, PotionItem, Requirement, Recipe, CraftingIngredient
+
+TEST_ITEM_NAME = "Test"
+TEST_ITEM_VALUE = 1
+TEST_ITEM_WEIGHT = 0.5
+TEST_ITEM_ID = 100
+
+TEST_EFFECT_NAME = "TestEffect"
+TEST_EFFECT_DESC = "Description"
+TEST_EFFECT_COST = 1
+TEST_EFFECT_MAG = 2
+TEST_EFFECT_DUR = 3
+TEST_EFFECT_VALUE = 4
+TEST_EFFECT_NEG = True
+TEST_EFFECT_VARYDUR = False
+TEST_EFFECT_ID = 200
+
+TEST_REQUIREMENT_NAME = "Requirement"
+TEST_REQUIREMENT_DESC = "This is a test"
+
+TEST_RECIPE_QUANTITY = 2
+
+class TestWrappers(unittest.TestCase):
+    def setUp(self):
+        self.db = Database("sqlite:///:memory:")
+        testitem = self.db.new_item(
+            TEST_ITEM_NAME, TEST_ITEM_VALUE, TEST_ITEM_WEIGHT,
+            TEST_ITEM_ID
+        )
+        self.db.new_potion_effect(
+            TEST_EFFECT_NAME, TEST_EFFECT_DESC, TEST_EFFECT_COST,
+            TEST_EFFECT_MAG, TEST_EFFECT_DUR, TEST_EFFECT_VALUE,
+            TEST_EFFECT_NEG, TEST_EFFECT_VARYDUR, TEST_EFFECT_ID
+        )
+        testreq = self.db.add_requirement(
+            TEST_REQUIREMENT_NAME, TEST_REQUIREMENT_DESC
+        )
+        self.db.commit()
+        testrecipe = self.db.add_recipe(
+            testitem, TEST_RECIPE_QUANTITY, testreq
+        )
+        self.db.commit()
+        self.db.add_ingredient_to_recipe(testrecipe, testitem)
+        self.db.commit()
+
+    def tearDown(self):
+        self.db.close_session()
+        m = MetaData()
+        m.reflect(self.db.engine)
+        m.drop_all(self.db.engine)
+        self.db.engine.dispose()
+        self.db = None
+
+    def assertTestItem(self, item):
+        self.assertEqual(
+            item.name, TEST_ITEM_NAME, "Name does not match"
+        )
+        self.assertEqual(
+            item.value, TEST_ITEM_VALUE, "Value does not match"
+        )
+        self.assertEqual(
+            item.weight, TEST_ITEM_WEIGHT, "Weight does not match"
+        )
+        self.assertEqual(item.id, TEST_ITEM_ID, "ID does not match")
+
+    def assertTestEffect(self, effect: PotionEffect):
+        self.assertEqual(
+            effect.name, TEST_EFFECT_NAME, "Name does not match"
+        )
+        self.assertEqual(
+            effect.description, TEST_EFFECT_DESC,
+            "Description does not match"
+        )
+        self.assertEqual(
+            effect.base_cost, TEST_EFFECT_COST, "Cost does not match"
+        )
+        self.assertEqual(
+            effect.base_mag, TEST_EFFECT_MAG,
+            "Magnitude does not match"
+        )
+        self.assertEqual(
+            effect.base_dur, TEST_EFFECT_DUR, "Duration does not match"
+        )
+        self.assertEqual(
+            effect.value, TEST_EFFECT_VALUE, "Value does not match"
+        )
+        self.assertEqual(
+            effect.is_negative, TEST_EFFECT_NEG,
+            "is_negative does not match"
+        )
+        self.assertEqual(
+            effect.vary_duration, TEST_EFFECT_VARYDUR,
+            "vary_duration does not match"
+        )
+        self.assertEqual(
+            effect.id, TEST_EFFECT_ID, "ID does not match"
+        )
+
+    def assertTestRequirement(self, req: Requirement):
+        self.assertEqual(
+            req.name, TEST_REQUIREMENT_NAME, "Name does not match"
+        )
+        self.assertEqual(
+            req.desc, TEST_REQUIREMENT_DESC,
+            "Description does not match"
+        )
+
+    def assertTestRecipe(self, recipe: Recipe):
+        self.assertEqual(
+            recipe.result_id, TEST_ITEM_ID, "Wrong item ID"
+        )
+        self.assertEqual(
+            recipe.requirement.name, TEST_REQUIREMENT_NAME,
+            "Wrong requirement name"
+        )
+        self.assertEqual(
+            recipe.quantity, TEST_RECIPE_QUANTITY, "Wrong quantity"
+        )
+
+    def test_new_item(self):
+        items = self.db.session.query(Item).filter_by(
+            name=TEST_ITEM_NAME
+        ).all()
+        self.assertEqual(len(items), 1, "Should only be one item")
+        self.assertTestItem(items[0])
+
+    def test_get_item_by_id(self):
+        item = self.db.get_item_by_ID(TEST_ITEM_ID)
+        self.assertTestItem(item)
+
+    def test_get_items_by_name(self):
+        items = self.db.get_items_by_name(TEST_ITEM_NAME)
+        self.assertEqual(len(items), 1)
+        self.assertTestItem(items[0])
+
+    def test_new_potion_effect(self):
+        effect = self.db.session.query(PotionEffect).filter_by(
+            id=TEST_EFFECT_ID
+        ).first()
+        self.assertTestEffect(effect)
+
+    def test_get_effect_by_ID(self):
+        effect = self.db.get_effect_by_ID(TEST_EFFECT_ID)
+        self.assertTestEffect(effect)
+
+    def test_get_effect_by_name(self):
+        effects = self.db.get_effects_by_name(TEST_EFFECT_NAME)
+        self.assertEqual(len(effects), 1)
+        self.assertTestEffect(effects[0])
+
+    def test_effect_association(self):
+        item = self.db.get_item_by_ID(TEST_ITEM_ID)
+        effect = self.db.get_effect_by_ID(TEST_EFFECT_ID)
+        self.db.add_effect_to_ingredient(effect, item, 1, 2, 3)
+        self.db.commit()
+
+        potionitems = self.db.get_effects_by_item(item)
+        self.assertEqual(len(potionitems), 1)
+        self.assertTestEffect(potionitems[0].effect)
+        self.assertTestItem(potionitems[0].item)
+
+        potionitems = self.db.get_items_by_effect(effect)
+        self.assertEqual(len(potionitems), 1)
+        self.assertTestEffect(potionitems[0].effect)
+        self.assertTestItem(potionitems[0].item)
+
+    def test_add_requirement(self):
+        req = self.db.session.query(Requirement).filter_by(
+            name=TEST_REQUIREMENT_NAME
+        ).first()
+        self.assertTestRequirement(req)
+
+    def test_get_requirements_by_name(self):
+        reqs = self.db.get_requirements_by_name(TEST_REQUIREMENT_NAME)
+        self.assertEqual(len(reqs), 1)
+        self.assertTestRequirement(reqs[0])
+
+    def test_add_recipe(self):
+        recipe = self.db.session.query(Recipe).first()
+        self.assertTestRecipe(recipe)
+
+    def test_get_ingredients_for_recipe(self):
+        recipe = self.db.session.query(Recipe).first()
+        ingredients = self.db.get_ingredients_for_recipe(recipe)
+        self.assertEqual(len(ingredients), 1)
+        self.assertTestItem(ingredients[0].ingredient)
+        self.assertTrue(recipe is ingredients[0].recipe)
+        self.assertEqual(ingredients[0].quantity, 1)
+
+if __name__ == '__main__':
+    unittest.main()