From 3ef3e72720567d540a890a99976accd4fb992d89 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Piotr=20Ma=C5=9Blanka?= <piotr.maslanka@henrietta.com.pl>
Date: Wed, 30 Sep 2020 17:19:47 +0200
Subject: [PATCH] add satella.dao, 2.11.20

---
 CHANGELOG.md            |  1 +
 docs/dao.rst            | 17 +++++++++++++
 docs/index.rst          |  1 +
 satella/__init__.py     |  2 +-
 satella/dao/__init__.py | 53 +++++++++++++++++++++++++++++++++++++++++
 tests/test_dao.py       | 20 ++++++++++++++++
 6 files changed, 93 insertions(+), 1 deletion(-)
 create mode 100644 docs/dao.rst
 create mode 100644 satella/dao/__init__.py
 create mode 100644 tests/test_dao.py

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc1d10fd..eae85507 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,3 +3,4 @@
 * unit testing engine changed from nose2 to pytest, since nose2 couldn't run tests on multiple threads
     It's much faster this way.
 * added extra wait for the futures in `sync_threadpool`
+* added `satella.dao`
diff --git a/docs/dao.rst b/docs/dao.rst
new file mode 100644
index 00000000..30da9b4c
--- /dev/null
+++ b/docs/dao.rst
@@ -0,0 +1,17 @@
+===
+DAO
+===
+
+This is for objects that are meant to represent a database entry,
+and are lazily loadable.
+
+It's constructor expects identifier and a keyword argument of load_lazy, which will control
+when will the object be fetched from DB.
+
+If True, then it will be fetched at constructor time, ie. the constructor will call .refresh().
+If False, then it will be fetched when it is first requested, via `must_be_loaded` decorator.
+
+.. autoclass:: satella.dao.Loadable
+    :members:
+
+.. autofunction:: satella.dao.must_be_loaded
diff --git a/docs/index.rst b/docs/index.rst
index 72ccf786..7e77d41f 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -23,6 +23,7 @@ Visit the project's page at GitHub_!
            instrumentation/memory
            instrumentation/metrics
            exception_handling
+           dao
            json
            posix
            import
diff --git a/satella/__init__.py b/satella/__init__.py
index 5d0387b3..f1f3d0f4 100644
--- a/satella/__init__.py
+++ b/satella/__init__.py
@@ -1 +1 @@
-__version__ = '2.11.20_a2'
+__version__ = '2.11.20'
diff --git a/satella/dao/__init__.py b/satella/dao/__init__.py
new file mode 100644
index 00000000..201492fd
--- /dev/null
+++ b/satella/dao/__init__.py
@@ -0,0 +1,53 @@
+from abc import ABCMeta, abstractmethod
+
+from satella.coding.decorators.decorators import wraps
+
+__all__ = ['Loadable', 'must_be_loaded']
+
+class Loadable(metaclass=ABCMeta):
+    """
+    Any class that can be loaded lazily.
+
+    It's keyword argument, load_lazy is expected to control lazy loading. If set to True,
+    DB will be hit as a part of this object's constructor.
+
+    If False, you will need to load it on-demand via must_be_loaded decorator.
+    """
+
+    __slots__ = ('_loaded',)
+
+    def __init__(self, load_lazy: bool = False):
+        self._loaded: bool = False
+        if not load_lazy:
+            self.refresh()
+
+    @abstractmethod
+    def refresh(self, load_from=None) -> None:
+        """
+        Optionally provide a class to load this class from.
+
+        Override me, calling me in a super method.
+
+        :param load_from: serialized object. If not given, the DB will be hit
+        """
+        self._loaded = True
+
+
+def must_be_loaded(fun):
+    """
+    A decorator for Loadable's methods.
+
+    Assures that .refresh() is called prior to executing that method, ie. the object
+    is loaded from the DB
+    """
+
+    @wraps(fun)
+    def inner(self, *args, **kwargs):
+        assert isinstance(self,
+                          Loadable), 'must_be_loaded called with a class that does not subclass ' \
+                                     'Loadable'
+        if not self._loaded:
+            self.refresh()
+        return fun(self, *args, **kwargs)
+
+    return inner
diff --git a/tests/test_dao.py b/tests/test_dao.py
new file mode 100644
index 00000000..7d37808b
--- /dev/null
+++ b/tests/test_dao.py
@@ -0,0 +1,20 @@
+import unittest
+
+from satella.dao import Loadable, must_be_loaded
+
+
+class TestDAO(unittest.TestCase):
+    def test_something(self):
+        class Load(Loadable):
+            def __init__(self, load_lazy=False):
+                super().__init__(load_lazy=load_lazy)
+
+            @must_be_loaded
+            def method_accessed(self):
+                assert self._loaded
+
+            def refresh(self, load_from=None) -> None:
+                super().refresh(load_from=load_from)
+
+        l = Load()
+        l.method_accessed()
-- 
GitLab