From f646f92c38022825e677b2ae058bbfc3db9804d2 Mon Sep 17 00:00:00 2001
From: Martin Lange <martin.lange@ufz.de>
Date: Mon, 19 Jun 2023 14:29:32 +0200
Subject: [PATCH] implement collecting metadata from components and adapters

---
 src/finam/interfaces.py     | 10 ++++++++++
 src/finam/schedule.py       | 28 ++++++++++++++++++++++++++++
 src/finam/sdk/adapter.py    |  5 +++++
 src/finam/sdk/component.py  |  5 +++++
 tests/core/test_schedule.py | 25 +++++++++++++++++++++++++
 5 files changed, 73 insertions(+)

diff --git a/src/finam/interfaces.py b/src/finam/interfaces.py
index 2379977d..d720281b 100644
--- a/src/finam/interfaces.py
+++ b/src/finam/interfaces.py
@@ -138,6 +138,11 @@ class IComponent(ABC):
     def status(self):
         """The component's current status."""
 
+    @property
+    @abstractmethod
+    def metadata(self):
+        """The component's meta data."""
+
 
 class ITimeComponent(IComponent, ABC):
     """Interface for components with a time step."""
@@ -453,6 +458,11 @@ class IOutput(ABC):
 class IAdapter(IInput, IOutput, ABC):
     """Interface for adapters."""
 
+    @property
+    @abstractmethod
+    def metadata(self):
+        """The adapter's meta data."""
+
 
 class NoBranchAdapter:
     """Interface to mark adapters as allowing only a single end point."""
diff --git a/src/finam/schedule.py b/src/finam/schedule.py
index e85a40a3..ecc2d36a 100644
--- a/src/finam/schedule.py
+++ b/src/finam/schedule.py
@@ -428,6 +428,34 @@ class Composition(Loggable):
                     f"Expecting one of [{', '.join(map(str, desired_list))}]"
                 )
 
+    @property
+    def metadata(self):
+        """
+        Meta data for all components and adapters.
+        Can only be used after ``connect``.
+
+        Raises
+        ------
+        FinamStatusError
+            Raises the error if ``connect`` was not called.
+        """
+        if not self.is_connected:
+            with ErrorLogger(self.logger):
+                raise FinamStatusError(
+                    "can't get meta data for a composition before connect was called"
+                )
+
+        md = {}
+        for mod in self.modules:
+            key = f"{mod.name}@{id(mod)}"
+            md[key] = mod.metadata
+
+        for ada in self.adapters:
+            key = f"{ada.name}@{id(ada)}"
+            md[key] = ada.metadata
+
+        return md
+
 
 def _collect_adapters_input(inp: IInput, out_adapters: set):
     src = inp.get_source()
diff --git a/src/finam/sdk/adapter.py b/src/finam/sdk/adapter.py
index 1f5c3d39..bdc8a728 100644
--- a/src/finam/sdk/adapter.py
+++ b/src/finam/sdk/adapter.py
@@ -67,6 +67,11 @@ class Adapter(IAdapter, Input, Output, ABC):
         """bool: if the adapter needs push."""
         return False
 
+    @property
+    def metadata(self):
+        """The adapter's meta data."""
+        return {}
+
     @final
     def push_data(self, data, time):
         """Push data into the output.
diff --git a/src/finam/sdk/component.py b/src/finam/sdk/component.py
index baf3610d..ef38a0dc 100644
--- a/src/finam/sdk/component.py
+++ b/src/finam/sdk/component.py
@@ -218,6 +218,11 @@ class Component(IComponent, Loggable, ABC):
         """Component name."""
         return self._name
 
+    @property
+    def metadata(self):
+        """The component's meta data."""
+        return {}
+
     @property
     def logger_name(self):
         """Logger name derived from base logger name and class name."""
diff --git a/tests/core/test_schedule.py b/tests/core/test_schedule.py
index e0a7e6b6..d9f36822 100644
--- a/tests/core/test_schedule.py
+++ b/tests/core/test_schedule.py
@@ -965,6 +965,31 @@ class TestComposition(unittest.TestCase):
         self.assertEqual([1, 8, 13], updates["A"])
         self.assertEqual([1, 2, 5, 8, 11], updates["B"])
 
+    def test_metadata(self):
+        module1 = MockupComponent(
+            callbacks={"Output": lambda t: t.day}, step=timedelta(1.0)
+        )
+        module2 = MockupDependentComponent(step=timedelta(1.0))
+
+        composition = Composition([module2, module1])
+        composition.initialize()
+
+        ada1 = fm.adapters.Scale(1.0)
+        ada2 = fm.adapters.Scale(1.0)
+        module1.outputs["Output"] >> ada1 >> ada2 >> module2.inputs["Input"]
+
+        with self.assertRaises(FinamStatusError) as context:
+            _ = composition.metadata
+
+        composition.connect()
+
+        md = composition.metadata
+
+        self.assertIn(f"{module1.name}@{id(module1)}", md)
+        self.assertIn(f"{module2.name}@{id(module2)}", md)
+        self.assertIn(f"{ada1.name}@{id(ada1)}", md)
+        self.assertIn(f"{ada2.name}@{id(ada2)}", md)
+
 
 if __name__ == "__main__":
     unittest.main()
-- 
GitLab