Passed
Pull Request — master (#22)
by Konstantinos
03:08 queued 01:32
created

so_magic.data.backend.engine   A

Complexity

Total Complexity 18

Size/Duplication

Total Lines 230
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 91
dl 0
loc 230
rs 10
c 0
b 0
f 0
wmc 18

11 Methods

Rating   Name   Duplication   Size   Complexity  
A CommandRegistrator.func_decorator() 0 8 2
A EngineType.dec() 0 28 2
A MyDecorator.magic_decorator() 0 14 2
A EngineType.__new__() 0 12 2
A CommandRegistrator.__getitem__() 0 5 2
A DataEngine.register_as_subclass() 0 16 1
A EngineType._generic_cmd_receiver() 0 27 2
A CommandRegistrator.__new__() 0 5 1
A EngineType._observations_from_file_cmd_receiver() 0 39 1
A DataEngine.new() 0 18 1
A EngineType._build_command() 0 17 2
1
"""This module defines a way to create Data Engines and to register new commands that a Data Engine can execute.
2
"""
3
from collections import defaultdict
4
from typing import Tuple, Callable
5
from .engine_command_factory import MagicCommandFactory
6
7
8
class MyDecorator(type):
9
    """Metaclass that provides a decorator able to be invoked both with and without parenthesis.
10
    The wrapper function logic should be implemented by the client code.
11
    """
12
    @classmethod
13
    def magic_decorator(mcs, arg=None):
14
        def decorator(_func):
15
            def wrapper(*a, **ka):
16
                ffunc = a[0]
17
                mcs._wrapper(ffunc, *a[1:], **ka)
18
                return ffunc
19
            return wrapper
20
21
        if callable(arg):
22
            _ = decorator(arg)
23
            return _  # return 'wrapper'
24
        _ = decorator
25
        return _  # ... or 'decorator'
26
27
28
class CommandRegistrator(MyDecorator):
29
    """Classes can use this class as metaclass to obtain a single registration point accessible as class attribute.
30
    """
31
    def __new__(mcs, *args, **kwargs):
32
        class_object = super().__new__(mcs, *args, **kwargs)
33
        class_object.state = None
34
        class_object.registry = {}
35
        return class_object
36
37
    def __getitem__(cls, item):
38
        if item not in cls.registry:
39
            raise RuntimeError(f"Key '{item}' fot found in registry: "
40
                               f"[{', '.join(str(x) for x in cls.registry.keys())}]")
41
        return cls.registry[item]
42
43
    # Legacy feature, not currently used in production
44
    def func_decorator(cls):
45
        def wrapper(a_callable):
46
            if hasattr(a_callable, '__code__'):  # it a function (def func_name ..)
47
                cls.registry[a_callable.__code__.co_name] = a_callable
48
            else:
49
                raise RuntimeError(f"Expected a function to be decorated; got {type(a_callable)}")
50
            return a_callable
51
        return wrapper
52
53
54
class EngineType(CommandRegistrator):
55
    """Tabular Data Backend type representation.
56
57
    Classes using this class as metaclass gain certain class attributes such as
58
    attributes related to tabular data operations (retriever, iterator, mutator) and attributes related to constructing
59
    command object prototypes (command_factory attribute).
60
    """
61
62
    def __new__(mcs, *args, **kwargs):
63
        engine_type = super().__new__(mcs, *args, **kwargs)
64
        engine_type._commands = {}
65
        engine_type.retriever = None
66
        engine_type.iterator = None
67
        engine_type.mutator = None
68
        engine_type.backend = None
69
        engine_type.command = mcs.magic_decorator
70
        engine_type.command_factory = MagicCommandFactory()
71
        engine_type._receivers = defaultdict(lambda: engine_type._generic_cmd_receiver,
72
                                             observations=engine_type._observations_from_file_cmd_receiver)
73
        return engine_type
74
75
    def _observations_from_file_cmd_receiver(cls, callable_function, **kwargs) -> Tuple[callable, dict]:
76
        """Create the Receiver of a command that creates datapoints from a file.
77
78
        It also creates the kwargs that a Command factory method would need along with the receiver object.
79
80
        It is assumed that the business logic is executed in the callable function supplied.
81
        You can use the data_structure "keyword" argument (kwarg) to indicate how should we parse/read
82
        the raw data from the file. Supported values: 'tabular-data'
83
84
        Args:
85
            callable_function (callable): the business logic that shall run in the command
86
87
        Returns:
88
            Union[callable, dict]: the receiver object that can be used to create a Command instance
89
                                    and parameters to pass in the kwargs of the command factory method (eg
90
                                    cls.command_factory(a_function, **kwargs_dict))
91
        """
92
93
        def observations(file_path, **runtime_kwargs):
94
            """Construct the observations attribute of a Datapoints instance.
95
96
            The signature of this function determines the signature that is used at runtime
97
            when the command will be executed. Thus the command's arguments at runtime
98
            should follow the signature of this function.
99
100
            Args:
101
                file_path (str): the file in disk that contains the data to be read into observations
102
            """
103
            # create the observations object
104
            _observations = callable_function(file_path, **runtime_kwargs)
105
            # create the datapoints object and let the datapoints factory notify its listeners (eg a datapoints manager)
106
            _ = cls.backend.datapoints_factory.create(kwargs.get('data_structure', 'tabular-data'),
107
                                                      _observations, [],
108
                                                      cls.retriever(),
109
                                                      cls.iterator(),
110
                                                      cls.mutator(),
111
                                                      file_path=file_path)
112
113
        return observations, {}
114
115
    def _generic_cmd_receiver(cls, callable_function, **kwargs) -> Tuple[callable, dict]:
116
        """Create the Receiver of a generic command.
117
118
        It also creates the kwargs that a Command factory method would need along with the receiver object.
119
120
        It is assumed that the business logic is executed in the callable function.
121
122
        Args:
123
            callable_function (Callable): the business logic that shall run in the command
124
125
        Returns:
126
            Union[callable, dict]: the receiver object that can be used to create a Command instance
127
                                    and parameters to pass in the kwargs of the command factory
128
                                    (eg cls.command_factory(a_function, **kwargs_dict))
129
        """
130
131
        def a_function(*args, **runtime_kwargs):
132
            """Just execute the business logic that is provided at runtime.
133
134
            The signature of this function determines the signature that is used at runtime
135
            when the command will be executed. Thus the command's arguments at runtime
136
            should follow the signature of this function. So, the runtime function
137
            can have any signature (since a_function uses flexible *args and **runtime_kwargs).
138
            """
139
            callable_function(*args, **runtime_kwargs)
140
141
        return a_function, {'name': lambda name: name}
142
143
    def _build_command(cls, a_callable: callable, registered_name: str, data_structure='tabular-data'):
144
        """Build a command given a callable object with the business logic and register the command under a name.
145
146
        Creates the required command Receiver and arguments, given a function at runtime. If the function is named
147
        'observations' then the Receiver is tailored to facilitate creating a Datapoints instance given a file path
148
        with the raw data.
149
150
        Args:
151
            a_callable (Callable): holds the business logic that executes when the command shall be executed
152
            registered_name (str): the name under which to register the command (can be used to reference the command)
153
            data_structure (str, optional): useful when creating a command that instantiates Datapoints objects.
154
            Defaults to 'tabular-data'.
155
        """
156
        receiver, kwargs_data = cls._receivers[registered_name](a_callable, data_structure=data_structure)
157
        cls.registry[registered_name] = receiver
158
        cls._commands[registered_name] = cls.command_factory(receiver, **{k: v for k, v in dict(kwargs_data, **{
159
            'name': kwargs_data.get('name', lambda name: '')(registered_name)}).items() if v})
160
161
    def dec(cls, data_structure='tabular-data') -> Callable[[Callable], Callable]:
162
        """Register a new command that executes the business logic supplied at runtime.
163
164
        Decorate a function so that its body acts as the business logic that runs as part of a Command.
165
        The name of the function can be used to later reference the Command (or a prototype object of the Command).
166
167
        Using the 'observations' name for your function will register a command that upon execution creates a new
168
        instance of Datapoints (see Datapoints class), provided that the runtime function returns an object that acts as
169
        the 'observations' attribute of a Datapoints object.
170
171
        Args:
172
            data_structure (str, optional): useful when the function name is 'observations'. Defaults to 'tabular-data'.
173
        """
174
175
        def wrapper(a_callable: Callable) -> Callable:
176
            """Build and register a new Command given a callable object that holds the important business logic.
177
178
            Args:
179
                a_callable (Callable): the Command's important underlying business logic
180
            """
181
            if hasattr(a_callable, '__code__'):  # a_callable object has been defined with the def python keyword
182
                decorated_function_name = a_callable.__code__.co_name
183
                cls._build_command(a_callable, decorated_function_name, data_structure=data_structure)
184
            else:
185
                raise RuntimeError(f"Expected a function to be decorated; got {type(a_callable)}")
186
            return a_callable
187
188
        return wrapper
189
190
191
class DataEngine(metaclass=EngineType):
192
    """Facility to create Data Engines."""
193
    subclasses = {}
194
195
    @classmethod
196
    def new(cls, engine_name: str) -> EngineType:
197
        """Create a Data Engine object and register it under the given name, to be able to reference it by name.
198
199
        Creates a Data Engine that serves as an empty canvas to add attributes and Commands.
200
201
        Args:
202
            engine_name (str): the name under which to register the Data Engine
203
204
        Returns:
205
            EngineType: the Data Engine object
206
        """
207
208
        @DataEngine.register_as_subclass(engine_name)
209
        class RuntimeDataEngine(DataEngine):
210
            pass
211
212
        return RuntimeDataEngine
213
214
    @classmethod
215
    def register_as_subclass(cls, engine_type: str):
216
        """Indicate that a class is a subclass of DataEngine and register it under the given name.
217
218
        It also sets the engine_type attribute on the decorate class to be equal to the subclass.
219
220
        Args:
221
            engine_type (str): the name under which to register the Data Engine
222
        """
223
224
        def wrapper(subclass) -> type:
225
            cls.subclasses[engine_type] = subclass
226
            setattr(cls, engine_type, subclass)
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable engine_type does not seem to be defined.
Loading history...
227
            return subclass
228
229
        return wrapper
230