From bbfb4f92af498ada25ab9279c37629c1b19695d3 Mon Sep 17 00:00:00 2001
From: Dmitry <b4tm4n@mail.ru>
Date: Tue, 20 Feb 2024 10:40:23 +0300
Subject: [PATCH] fmt + StateFileFilter

---
 pddnsc/base.py             | 14 ++++++-------
 pddnsc/cli.py              | 39 +++++++++++++------------------------
 pddnsc/filters/__init__.py |  2 +-
 pddnsc/filters/files.py    | 40 ++++++++++++++++++++++++++++++++++----
 pddnsc/loaders.py          | 25 ++++++++++++++++++++++++
 pddnsc/outputs/__init__.py |  4 ++--
 pddnsc/outputs/console.py  |  1 +
 pddnsc/outputs/files.py    | 10 +++++++---
 pddnsc/plugins.py          | 35 ++++++++++++++++-----------------
 pddnsc/sources/__init__.py |  2 +-
 pddnsc/sources/fake.py     | 21 +++++++++-----------
 pddnsc/sources/ipfy.py     |  1 +
 settings/config.toml       | 13 +++++++++----
 13 files changed, 128 insertions(+), 79 deletions(-)
 create mode 100644 pddnsc/loaders.py

diff --git a/pddnsc/base.py b/pddnsc/base.py
index f84c448..43fe738 100644
--- a/pddnsc/base.py
+++ b/pddnsc/base.py
@@ -2,6 +2,7 @@ import httpx
 import asyncio
 from abc import ABC, abstractmethod
 
+
 class BaseSourceProvider(ABC):
     _childs = {}
     registred = {}
@@ -46,12 +47,10 @@ class BaseSourceProvider(ABC):
         cls.registred[name] = provider(name, config, ipv4t, ipv6t)
 
     @abstractmethod
-    async def fetch_v4(self) -> str:
-        ...
+    async def fetch_v4(self) -> str: ...
 
     @abstractmethod
-    async def fetch_v6(self) -> str:
-        ...
+    async def fetch_v6(self) -> str: ...
 
 
 class BaseOutputProvider(ABC):
@@ -94,8 +93,8 @@ class BaseOutputProvider(ABC):
         return await self.set_addrs_imp(source_provider, addr_v4, addr_v6)
 
     @abstractmethod
-    async def set_addrs_imp(self, source_provider, addr_v4, addr_v6):
-        ...
+    async def set_addrs_imp(self, source_provider, addr_v4, addr_v6): ...
+
 
 class BaseFilterProvider(ABC):
     _childs = {}
@@ -137,5 +136,4 @@ class BaseFilterProvider(ABC):
         return await self.check_imp(source_provider, addr_v4, addr_v6)
 
     @abstractmethod
-    async def check_imp(self, source_provider, addr_v4, addr_v6):
-        ...
\ No newline at end of file
+    async def check_imp(self, source_provider, addr_v4, addr_v6): ...
diff --git a/pddnsc/cli.py b/pddnsc/cli.py
index 4e9ba29..483943e 100644
--- a/pddnsc/cli.py
+++ b/pddnsc/cli.py
@@ -3,9 +3,8 @@ import asyncio
 from abc import ABC, abstractmethod
 import toml
 from .base import BaseFilterProvider, BaseSourceProvider, BaseOutputProvider
-from . import sources
-from . import outputs
-from . import filters
+from .plugins import use_plugins
+
 
 async def source_task():
     providers = BaseSourceProvider.registred.values()
@@ -30,12 +29,6 @@ async def source_task():
     return result
 
 
-async def output_task(providers, result):
-    providers = BaseOutputProvider.registred.values()
-    await asyncio.gather(
-        *(asyncio.create_task(p.set_addrs(*result), name=p.name) for p in providers)
-    )
-
 async def filter_task(ip_result):
     providers = BaseFilterProvider.registred.values()
     result = True
@@ -55,29 +48,23 @@ async def filter_task(ip_result):
             await gather
         except asyncio.CancelledError:
             pass
-    
+
     if not result:
         print("failed filter:", failed)
 
     return result
-    
+
+
+async def output_task(result):
+    providers = BaseOutputProvider.registred.values()
+    await asyncio.gather(
+        *(asyncio.create_task(p.set_addrs(*result), name=p.name) for p in providers)
+    )
+
 
 async def app(ipv4t, ipv6t):
     config = toml.load("settings/config.toml")
-    for source_name in config["sources"]:
-        BaseSourceProvider.register_provider(
-            source_name, config["sources"][source_name], ipv4t, ipv6t
-        )
-
-    for filter_name in config["filters"]:
-        BaseFilterProvider.register_provider(
-            filter_name, config["filters"][filter_name], ipv4t, ipv6t
-        )
-
-    for output_name in config["outputs"]:
-        BaseOutputProvider.register_provider(
-            output_name, config["outputs"][output_name], ipv4t, ipv6t
-        )
+    use_plugins(config, ipv4t, ipv6t)
 
     debug = config.get("debug", False)
     if debug:
@@ -100,13 +87,13 @@ async def app(ipv4t, ipv6t):
         print(
             f"output providers: {[*BaseOutputProvider.registred]}, {[*map(str, BaseOutputProvider.registred.values())]}"
         )
-        #print(config)
 
     result = await source_task()
     if not await filter_task(result):
         print("stop by filters")
         return
     await output_task(result)
+    print("done")
 
 
 async def main():
diff --git a/pddnsc/filters/__init__.py b/pddnsc/filters/__init__.py
index 1587c10..6be9257 100644
--- a/pddnsc/filters/__init__.py
+++ b/pddnsc/filters/__init__.py
@@ -1,3 +1,3 @@
-from pddnsc.plugins import load_plugins
+from pddnsc.loaders import load_plugins
 
 load_plugins(__file__)
diff --git a/pddnsc/filters/files.py b/pddnsc/filters/files.py
index 3680978..3330e2c 100644
--- a/pddnsc/filters/files.py
+++ b/pddnsc/filters/files.py
@@ -6,9 +6,10 @@ from os.path import isfile
 
 from pddnsc.base import BaseFilterProvider
 
+
 class StateHashFilter(BaseFilterProvider):
     async def check_imp(self, source_provider, addr_v4, addr_v6):
-        if not isfile(self.config['filepath']):
+        if not isfile(self.config["filepath"]):
             return True
 
         new_state = {
@@ -16,8 +17,39 @@ class StateHashFilter(BaseFilterProvider):
             "ipv6": addr_v6 or "",
         }
         new_state_str = json.dumps(new_state)
-        new_sha = hashlib.sha256(new_state_str.encode(encoding='utf-8'))
-        async with aiofiles.open(self.config['filepath'], mode='r', encoding='utf-8') as f:
+        new_sha = hashlib.sha256(new_state_str.encode(encoding="utf-8"))
+        async with aiofiles.open(
+            self.config["filepath"], mode="r", encoding="utf-8"
+        ) as f:
             old_state_hash = await f.read()
-        
+
         return old_state_hash != new_sha.hexdigest()
+
+
+class StateFileFilter(BaseFilterProvider):
+    async def check_imp(self, source_provider, addr_v4, addr_v6):
+        if not isfile(self.config["filepath"]):
+            return True
+
+        new_state = {
+            "ipv4": addr_v4 or "",
+            "ipv6": addr_v6 or "",
+        }
+
+        async with aiofiles.open(
+            self.config["filepath"], mode="r", encoding="utf-8"
+        ) as f:
+            old_state = json.loads(await f.read())
+
+        result = True
+
+        if "check_ipv4" not in self.config and "check_ipv4" not in self.config:
+            return new_state != old_state
+
+        if self.config.get("check_ipv4", False):
+            result = result and new_state["ipv4"] != old_state["ipv4"]
+
+        if self.config.get("check_ipv6", False):
+            result = result and new_state["ipv6"] != old_state["ipv6"]
+
+        return result
diff --git a/pddnsc/loaders.py b/pddnsc/loaders.py
new file mode 100644
index 0000000..00ee9f5
--- /dev/null
+++ b/pddnsc/loaders.py
@@ -0,0 +1,25 @@
+import os
+import traceback
+from importlib import util
+
+
+def load_module(path):
+    name = os.path.split(path)[-1]
+    spec = util.spec_from_file_location(name, path)
+    module = util.module_from_spec(spec)
+    spec.loader.exec_module(module)
+    return module
+
+
+def load_plugins(init_filepath):
+    dirpath = os.path.dirname(os.path.abspath(init_filepath))
+    for fname in os.listdir(dirpath):
+        if (
+            not fname.startswith(".")
+            and not fname.startswith("__")
+            and fname.endswith(".py")
+        ):
+            try:
+                load_module(os.path.join(dirpath, fname))
+            except Exception:
+                traceback.print_exc()
diff --git a/pddnsc/outputs/__init__.py b/pddnsc/outputs/__init__.py
index c97dfea..6be9257 100644
--- a/pddnsc/outputs/__init__.py
+++ b/pddnsc/outputs/__init__.py
@@ -1,3 +1,3 @@
-from pddnsc.plugins import load_plugins
+from pddnsc.loaders import load_plugins
 
-load_plugins(__file__)
\ No newline at end of file
+load_plugins(__file__)
diff --git a/pddnsc/outputs/console.py b/pddnsc/outputs/console.py
index 36a2062..93495e9 100644
--- a/pddnsc/outputs/console.py
+++ b/pddnsc/outputs/console.py
@@ -2,6 +2,7 @@ import asyncio
 
 from pddnsc.base import BaseOutputProvider
 
+
 class JustPrint(BaseOutputProvider):
     async def set_addrs_imp(self, source_provider, addr_v4, addr_v6):
         print(f">> {self.name}")
diff --git a/pddnsc/outputs/files.py b/pddnsc/outputs/files.py
index 13a18df..b0ab7c2 100644
--- a/pddnsc/outputs/files.py
+++ b/pddnsc/outputs/files.py
@@ -13,7 +13,9 @@ class StateFile(BaseOutputProvider):
             "ipv6": addr_v6 or "",
         }
         state_str = json.dumps(state)
-        async with aiofiles.open(self.config['filepath'], mode='w', encoding='utf-8') as f:
+        async with aiofiles.open(
+            self.config["filepath"], mode="w", encoding="utf-8"
+        ) as f:
             await f.write(state_str)
 
 
@@ -24,6 +26,8 @@ class StateHashFile(BaseOutputProvider):
             "ipv6": addr_v6 or "",
         }
         state_str = json.dumps(state)
-        sha = hashlib.sha256(state_str.encode(encoding='utf-8'))
-        async with aiofiles.open(self.config['filepath'], mode='w', encoding='utf-8') as f:
+        sha = hashlib.sha256(state_str.encode(encoding="utf-8"))
+        async with aiofiles.open(
+            self.config["filepath"], mode="w", encoding="utf-8"
+        ) as f:
             await f.write(sha.hexdigest())
diff --git a/pddnsc/plugins.py b/pddnsc/plugins.py
index be09711..8547462 100644
--- a/pddnsc/plugins.py
+++ b/pddnsc/plugins.py
@@ -1,22 +1,21 @@
-import os
-import traceback
-from importlib import util
+from .base import BaseSourceProvider, BaseFilterProvider, BaseOutputProvider
+from . import sources
+from . import outputs
+from . import filters
 
 
-def load_module(path):
-    name = os.path.split(path)[-1]
-    spec = util.spec_from_file_location(name, path)
-    module = util.module_from_spec(spec)
-    spec.loader.exec_module(module)
-    return module
+def use_plugins(config, ipv4t, ipv6t):
+    for source_name in config["sources"]:
+        BaseSourceProvider.register_provider(
+            source_name, config["sources"][source_name], ipv4t, ipv6t
+        )
 
+    for filter_name in config["filters"]:
+        BaseFilterProvider.register_provider(
+            filter_name, config["filters"][filter_name], ipv4t, ipv6t
+        )
 
-def load_plugins(init_filepath):
-    dirpath = os.path.dirname(os.path.abspath(init_filepath))
-    for fname in os.listdir(dirpath):
-        if not fname.startswith('.') and \
-        not fname.startswith('__') and fname.endswith('.py'):
-            try:
-                load_module(os.path.join(dirpath, fname))
-            except Exception:
-                traceback.print_exc()
+    for output_name in config["outputs"]:
+        BaseOutputProvider.register_provider(
+            output_name, config["outputs"][output_name], ipv4t, ipv6t
+        )
diff --git a/pddnsc/sources/__init__.py b/pddnsc/sources/__init__.py
index 1587c10..6be9257 100644
--- a/pddnsc/sources/__init__.py
+++ b/pddnsc/sources/__init__.py
@@ -1,3 +1,3 @@
-from pddnsc.plugins import load_plugins
+from pddnsc.loaders import load_plugins
 
 load_plugins(__file__)
diff --git a/pddnsc/sources/fake.py b/pddnsc/sources/fake.py
index ac0254e..cbf1284 100644
--- a/pddnsc/sources/fake.py
+++ b/pddnsc/sources/fake.py
@@ -3,29 +3,26 @@ import asyncio
 
 from pddnsc.base import BaseSourceProvider
 
+
 class DummySource(BaseSourceProvider):
     async def fetch_v4(self) -> str:
-        async with httpx.AsyncClient(transport=self.ipv4t) as client:
-            result = await asyncio.sleep(10, result=None)
-        result = await asyncio.sleep(10, result=None)
+        result = await asyncio.sleep(self.config.get("delay", 1), result=None)
         return result
 
     async def fetch_v6(self) -> str:
-        async with httpx.AsyncClient(transport=self.ipv6t) as client:
-            result = await asyncio.sleep(10, result=None)
-        result = await asyncio.sleep(10, result=None)
+        result = await asyncio.sleep(self.config.get("delay", 1), result=None)
         return result
 
 
 class FakeSource(BaseSourceProvider):
     async def fetch_v4(self) -> str:
-        async with httpx.AsyncClient(transport=self.ipv4t) as client:
-            result = await asyncio.sleep(
-                3.3, result=self.config.get("ipv4", "127.0.0.1")
-            )
+        result = await asyncio.sleep(
+            self.config.get("delay", 1), result=self.config.get("ipv4", "127.0.0.1")
+        )
         return result
 
     async def fetch_v6(self) -> str:
-        async with httpx.AsyncClient(transport=self.ipv6t) as client:
-            result = await asyncio.sleep(4.4, result=self.config.get("ipv6", "::1"))
+        result = await asyncio.sleep(
+            self.config.get("delay", 1), result=self.config.get("ipv6", "::1")
+        )
         return result
diff --git a/pddnsc/sources/ipfy.py b/pddnsc/sources/ipfy.py
index 5ab3095..8a06d49 100644
--- a/pddnsc/sources/ipfy.py
+++ b/pddnsc/sources/ipfy.py
@@ -2,6 +2,7 @@ import httpx
 
 from pddnsc.base import BaseSourceProvider
 
+
 class IPIFYSource(BaseSourceProvider):
     async def fetch_v4(self) -> str:
         async with httpx.AsyncClient(transport=self.ipv4t) as client:
diff --git a/settings/config.toml b/settings/config.toml
index 4d3fd42..e50a56a 100644
--- a/settings/config.toml
+++ b/settings/config.toml
@@ -1,21 +1,26 @@
 debug = true
 
 [sources]
-  [sources.test1-src]
+  [sources.ipfy]
     provider = "IPIFYSource"
-  [sources.test2-src]
+  [sources.fake]
     provider = "FakeSource"
+    delay = 10
     ipv6 = "fe80::1"
 
 [filters]
+  [filters.state-file]
+    provider = "StateFileFilter"
+    filepath = "state/state.json"
+    check_ipv4 = true
   [filters.state-hash]
     provider = "StateHashFilter"
     filepath = "state/hash.txt"
-    
+
 [outputs]
   [outputs.print]
     provider = "JustPrint"
-  [outputs.file]
+  [outputs.state-file]
     provider = "StateFile"
     filepath = "state/state.json"
   [outputs.hash-file]