Repository: sosw/sosw

Issue:
Implement get_ddbc for custom row_mappers
Implement in Processor something like this and add documentation and tests:

```
    def get_ddbc(self, prefix: str) -> DynamoDbClient:
        """ Lazy init of custom DDB clients. """
        names = list([x.split('_dynamo_db_config')[0] for x in
                      filter(lambda x: x.endswith('_dynamo_db_config'), self.config)])
        if prefix not in names:
            raise ValueError(f"get_ddbc() method supports only prefixes: {names}")

        name = f"{prefix}_dynamo_db_client"
        if not getattr(self, name, None):
            setattr(self, name, DynamoDbClient(self.config[f'{prefix}_dynamo_db_config']))

        return getattr(self, name)
```

Discussion:

Patch:
diff --git a/docs/essentials/processor.rst b/docs/essentials/processor.rst
index afa7c87..481e3b4 100644
--- a/docs/essentials/processor.rst
+++ b/docs/essentials/processor.rst
@@ -5,5 +5,7 @@ Processor
 
 Core class for other components to inherit.
 
-.. automodule:: sosw.app
-   :members:
+..  automodule:: sosw.app
+    :members:
+    :private-members:
+
diff --git a/sosw/app.py b/sosw/app.py
index aee950a..0b5836c 100644
--- a/sosw/app.py
+++ b/sosw/app.py
@@ -46,10 +46,10 @@ import os
 from collections import defaultdict
 from importlib import import_module
 from typing import Dict
-
 from sosw.components.benchmark import benchmark
 from sosw.components.config import get_config
 from sosw.components.helpers import *
+from sosw.components.dynamo_db import DynamoDbClient
 
 
 class Processor:
@@ -57,12 +57,30 @@ class Processor:
     Core Processor class template. All the main components (Worker, Orchestrator and Scheduler) inherit from this one.
     You can also use this class as parent for some of your standalone Lambdas, but we strictly encourage you to use
     `Worker` class in case you are running functions under `sosw` orchestration.
+
+
+    ``get_ddbc(prefix: str) -> DynamoDbClient:``
+
+    Lazily initializes and retrieves a DynamoDB client configured for a specific table and schema validation.
+
+    This method initializes custom DynamoDB clients based on the provided prefix. If a client with the
+    specified prefix has already been initialized, it returns the existing client. If not, it looks in the
+    Processor config for a prefixed dynamodb config (e.g. for prefix ``project_a`` -> ``project_a_dynamo_db_config``).
+    The dynamo_db client will be initialized as ``self.project_a_dynamo_db_client``.
+
+    This is particularly useful for scenarios requiring schema validation, transformation of DynamoDB
+    syntax to dictionary format, and other operations beyond the capabilities of the raw boto3 client.
+
+    :param str prefix:  The prefix for the DynamoDB client configuration and naming.
+    :raises ValueError: If the provided prefix is not supported by the available configuration.
+
     """
 
     DEFAULT_CONFIG = {}
 
     aws_account = None
     aws_region = os.getenv('AWS_REGION', None)
+    ddb_names = None
     lambda_context = None
 
 
@@ -90,7 +108,7 @@ class Processor:
 
     def init_config(self, custom_config: Dict = None):
         """
-        By default, tries to initialize config from DEFAULT_CONFIG or as an empty dictionary.
+        By default, tries to initialize config from ``DEFAULT_CONFIG`` or as an empty dictionary.
         After that, a specific custom config of the Lambda will recursively update the existing one.
         The last step is update config recursively with a passed custom_config.
 
@@ -257,6 +275,36 @@ class Processor:
         return self.aws_region
 
 
+    @benchmark
+    def get_ddbc(self, prefix: str) -> DynamoDbClient:
+        """
+        Lazily initializes and retrieves a DynamoDB client configured for a specific table and schema validation.
+
+        This method initializes custom DynamoDB clients based on the provided prefix. If a client with the
+        specified prefix has already been initialized, it returns the existing client. If not, it looks in the
+        Processor config for a prefixed dynamodb config (e.g. for prefix ``project_a`` -> ``project_a_dynamo_db_config``).
+        The dynamo_db client will be initialized as ``self.project_a_dynamo_db_client``.
+
+        This is particularly useful for scenarios requiring schema validation, transformation of DynamoDB
+        syntax to dictionary format, and other operations beyond the capabilities of the raw boto3 client.
+
+        :param str prefix:  The prefix for the DynamoDB client configuration and naming.
+        :raises ValueError: If the provided prefix is not supported by the available configuration.
+        """
+        if not self.ddb_names:
+            self.ddb_names = list([x.split('_dynamo_db_config')[0] for x in
+                                   filter(lambda x: x.endswith('_dynamo_db_config'), self.config)])
+
+        if prefix not in self.ddb_names:
+            raise ValueError(f"get_ddbc() method supports only prefixes: {self.ddb_names}")
+
+        name = f"{prefix}_dynamo_db_client"
+        if not hasattr(self, name):
+            setattr(self, name, DynamoDbClient(self.config[f'{prefix}_dynamo_db_config']))
+
+        return getattr(self, name)
+
+
     def get_stats(self, recursive: bool = True):
         """
         Return statistics of operations performed by current instance of the Class.


Test patch:
diff --git a/sosw/test/test_app__aws.py b/sosw/test/test_app__aws.py
index 72eb8e5..ee7f786 100644
--- a/sosw/test/test_app__aws.py
+++ b/sosw/test/test_app__aws.py
@@ -1,14 +1,18 @@
+import asyncio
 import boto3
 import os
 import unittest
 
 from copy import deepcopy
-from unittest.mock import patch
+from unittest.mock import patch, MagicMock
 
 from ..app import Processor
-from ..components.sns import SnsManager
+from ..components.dynamo_db import DynamoDbClient
 from ..components.siblings import SiblingsManager
+from ..components.sns import SnsManager
+
 
+from sosw.test.helpers_test_dynamo_db import AutotestDdbManager, autotest_dynamo_db_setup, get_autotest_ddb_name
 
 os.environ["STAGE"] = "test"
 os.environ["autotest"] = "True"
@@ -20,6 +24,19 @@ class app_TestCase(unittest.TestCase):
         'siblings_config': {'test': True},
     }
 
+    autotest_ddbm: AutotestDdbManager = None
+
+
+    @classmethod
+    def setUpClass(cls) -> None:
+        tables = [autotest_dynamo_db_setup]
+        cls.autotest_ddbm = AutotestDdbManager(tables)
+
+
+    @classmethod
+    def tearDownClass(cls) -> None:
+        asyncio.run(cls.autotest_ddbm.drop_ddbs())
+
 
     def setUp(self):
         self.patcher = patch("sosw.app.get_config")
@@ -68,3 +85,34 @@ class app_TestCase(unittest.TestCase):
             'init_clients': ['NotExists', 'Sns']
         }
         self.assertRaises(RuntimeError, Processor, custom_config=custom_config)
+
+
+    def test_get_ddbc_initialization(self):
+        """
+         Tests the `get_ddbc` method of Processor class with a valid prefix and configuration.
+
+         This test verifies that:
+             * `mock_dynamodb_client` is called once with the correct arguments.
+             * The returned client instance is an instance of `DynamoDbClient`.
+         """
+
+        prefix = 'example'
+        config = {
+            'example_dynamo_db_config': {'table_name': get_autotest_ddb_name()},
+        }
+
+        processor = Processor(custom_config=config)
+        self.assertIsNone(getattr(processor, 'example_dynamo_db_client', None))
+
+        client_instance = processor.get_ddbc(prefix)
+
+        self.assertIsInstance(client_instance, DynamoDbClient)
+        self.assertIsInstance(getattr(processor, 'example_dynamo_db_client', None), DynamoDbClient)
+
+        for i in range(3):
+            new_client_instance = processor.get_ddbc(prefix)
+            self.assertEqual(new_client_instance, client_instance)
+
+        self.assertEqual(processor.get_stats()['calls_get_ddbc'], 4)
+
+
diff --git a/sosw/test/unit/test_app.py b/sosw/test/unit/test_app.py
index 9236d6e..dc768ca 100644
--- a/sosw/test/unit/test_app.py
+++ b/sosw/test/unit/test_app.py
@@ -215,3 +215,50 @@ class app_UnitTestCase(unittest.TestCase):
         self.assertIn('SoswWorkerErrors', kwargs['TopicArn'])
         self.assertEqual(kwargs['Subject'], 'Some Function died')
         self.assertEqual(kwargs['Message'], 'Unknown Failure')
+
+
+    @patch("boto3.client")
+    @patch("sosw.app.DynamoDbClient")
+    def test_get_ddbc(self, mock_dynamodb_client, _):
+        """
+         Tests the `get_ddbc` method of Processor class with a valid prefix and configuration.
+
+         This test verifies that:
+             * `mock_dynamodb_client` is called once with the correct arguments.
+             * The returned client instance is an instance of `DynamoDbClient`.
+         """
+
+        prefix = 'example'
+        config = {
+            'example_dynamo_db_config': {'table_name': 'example_table'},
+        }
+
+        # mock_dynamodb_client.return_value = MagicMock()
+
+        processor = Processor(custom_config=config)
+        client_instance = processor.get_ddbc(prefix)
+
+        mock_dynamodb_client.assert_called_once_with(config['example_dynamo_db_config'])
+        self.assertIsInstance(client_instance, MagicMock)
+
+
+    def test_get_ddbc_invalid_prefix(self):
+        """
+           Tests the `get_ddbc` method of Processor class when an invalid prefix is provided.
+
+           This test verifies that:
+               * A `ValueError` is raised when an invalid prefix is provided.
+               * The error message contains the expected message indicating the supported prefixes.
+           """
+
+        prefix = 'invalid'
+        config = {
+            'example_dynamo_db_config': {'table_name': 'example_table'},
+        }
+
+        processor = Processor(custom_config=config)
+
+        with self.assertRaises(ValueError) as context:
+            processor.get_ddbc(prefix)
+
+            self.assertEqual(str(context.exception), "get_ddbc() method supports only prefixes: ['example']")