diff --git a/elite_engineering/materials.py b/elite_engineering/materials.py index 25affae..8dbc284 100644 --- a/elite_engineering/materials.py +++ b/elite_engineering/materials.py @@ -27,13 +27,15 @@ def _load() -> typing.Dict[str, Material]: lines = path.read_text().splitlines() reader = csv.reader(lines[1:]) return { - symbol: Material(name, type_, category, int(grade)) + symbol.lower(): Material(name, type_, category, int(grade)) for fdevid, symbol, grade, type_, category, name in reader } materials: typing.Dict[str, Material] = _load() +_Inventory = typing.TypeVar("_Inventory", bound="Inventory") + class Inventory(collections.UserDict): """A mapping of every available material with an associated quantity.""" @@ -47,11 +49,18 @@ class Inventory(collections.UserDict): self.update(items) def __setitem__(self, key, value) -> None: - if key not in materials: + normalized_key = key.lower() + if normalized_key not in materials: raise KeyError(key) if value < 0: raise ValueError(value) - super().__setitem__(key, value) + super().__setitem__(normalized_key, value) + + def __getitem__(self, key: str) -> int: + normalized_key = key.lower() + if normalized_key not in materials: + raise KeyError(key) + return super().__getitem__(normalized_key) def has(self, items: typing.Union[typing.Dict[str, int], "Inventory"]) -> bool: return all([self[name] >= quantity for name, quantity in items.items()]) @@ -62,6 +71,23 @@ class Inventory(collections.UserDict): def __sub__(self, other: "Inventory") -> "Inventory": return Inventory({name: self[name] - qty for name, qty in other.items()}) + @classmethod + def from_event( + cls: typing.Type[_Inventory], event: typing.Dict[str, typing.Any] + ) -> _Inventory: + if event.get("event") != "Materials": + raise ValueError("Not a 'Materials' event") + return cls( + { + material["Name"]: material["Count"] + for material in [ + *event.get("Raw", []), + *event.get("Manufactured", []), + *event.get("Encoded", []), + ] + } + ) + class Materials(Inventory): """Same as Inventory, but omits zeroed materials.""" @@ -75,18 +101,20 @@ class Materials(Inventory): self.update(items) def __setitem__(self, key: str, value: int) -> None: - if key not in materials: + normalized_key = key.lower() + if normalized_key not in materials: raise KeyError(key) if value < 0: raise ValueError(value) if value == 0: return - super().__setitem__(key, value) + super().__setitem__(normalized_key, value) def __getitem__(self, key: str) -> int: - if key not in materials: + normalized_key = key.lower() + if normalized_key not in materials: raise KeyError(key) - return self.data.get(key, 0) + return self.data.get(normalized_key.lower(), 0) def __add__(self, other: "Inventory") -> "Materials": return Materials({name: self[name] + other[name] for name in materials}) diff --git a/tests/test_materials.py b/tests/test_materials.py index ef436c0..d472651 100644 --- a/tests/test_materials.py +++ b/tests/test_materials.py @@ -1,4 +1,6 @@ import fractions +import json +import pathlib import typing import unittest @@ -86,6 +88,9 @@ class InventoryTests(unittest.TestCase): ) -> "materials.Inventory": return materials.Inventory(items) + def from_event(self, event: typing.Dict[str, typing.Any]) -> "materials.Inventory": + return materials.Inventory.from_event(event) + def test_create_empty(self) -> None: inventory = self.make() self.assertEqual(set(materials.materials.keys()), set(inventory.keys())) @@ -140,6 +145,83 @@ class InventoryTests(unittest.TestCase): a - b self.assertEqual(exc.exception.args, (-1,)) + def test_load_from_materials_event(self) -> None: + event = json.loads( + pathlib.Path(__file__).parent.joinpath("events/materials.json").read_text() + ) + event = { + "timestamp": "2022-01-23T02:58:12Z", + "event": "Materials", + "Raw": [ + {"Name": "manganese", "Count": 127}, + {"Name": "iron", "Count": 37}, + {"Name": "phosphorus", "Count": 2}, + {"Name": "nickel", "Count": 5}, + {"Name": "lead", "Count": 69}, + {"Name": "germanium", "Count": 12}, + ], + "Manufactured": [ + { + "Name": "salvagedalloys", + "Name_Localised": "Salvaged Alloys", + "Count": 1, + }, + { + "Name": "crystalshards", + "Name_Localised": "Crystal Shards", + "Count": 36, + }, + { + "Name": "shieldemitters", + "Name_Localised": "Shield Emitters", + "Count": 36, + }, + ], + "Encoded": [ + { + "Name": "bulkscandata", + "Name_Localised": "Anomalous Bulk Scan Data", + "Count": 28, + }, + { + "Name": "shieldcyclerecordings", + "Name_Localised": "Distorted Shield Cycle Recordings", + "Count": 66, + }, + { + "Name": "archivedemissiondata", + "Name_Localised": "Irregular Emission Data", + "Count": 6, + }, + ], + } + + self.assertEqual( + self.make( + { + "manganese": 127, + "iron": 37, + "phosphorus": 2, + "nickel": 5, + "lead": 69, + "germanium": 12, + "salvagedalloys": 1, + "crystalshards": 36, + "shieldemitters": 36, + "bulkscandata": 28, + "shieldcyclerecordings": 66, + "archivedemissiondata": 6, + } + ), + self.from_event(event), + ) + + def test_load_from_invalid_event(self) -> None: + event: typing.Dict[str, typing.Any] = {} + with self.assertRaises(ValueError) as exc: + self.from_event(event) + self.assertEqual(exc.exception.args, ("Not a 'Materials' event",)) + class MaterialsTests(InventoryTests): def make( @@ -147,6 +229,9 @@ class MaterialsTests(InventoryTests): ) -> "materials.Materials": return materials.Materials(items) + def from_event(self, event: typing.Dict[str, typing.Any]) -> "materials.Materials": + return materials.Materials.from_event(event) + def test_create_empty(self) -> None: empty = self.make() self.assertEqual(set(), set(empty.keys()))