kytos.core.napps.base   B
last analyzed

Complexity

Total Complexity 47

Size/Duplication

Total Lines 291
Duplicated Lines 0 %

Test Coverage

Coverage 93.48%

Importance

Changes 0
Metric Value
eloc 160
dl 0
loc 291
ccs 129
cts 138
cp 0.9348
rs 8.64
c 0
b 0
f 0
wmc 47

28 Methods

Rating   Name   Duplication   Size   Complexity  
A NApp.__init__() 0 11 2
A NApp.__hash__() 0 2 1
A NApp.as_json() 0 3 1
A NApp.__eq__() 0 3 1
A NApp.id() 0 4 1
A NApp.__str__() 0 2 1
A KytosNApp.shutdown() 0 3 1
A KytosNApp.execute() 0 3 1
A KytosNApp.setup() 0 3 1
A KytosNApp._shutdown_handler() 0 12 2
A NApp._update_repo_file() 0 4 2
A NApp.create_from_dict() 0 9 2
A KytosNApp.listeners() 0 3 1
A NApp.package_url() 0 6 2
A KytosNApp.notify_loaded() 0 5 1
A NApp._extract() 0 13 2
A KytosNApp._load_json() 0 9 3
A KytosNApp.napp_id() 0 4 1
A NApp.match() 0 9 2
A NApp.download() 0 18 2
A NApp.create_from_uri() 0 13 2
A NApp._has_valid_repository() 0 3 1
A KytosNApp.run() 0 11 3
A NApp.create_from_json() 0 7 2
A NApp.__repr__() 0 2 1
A KytosNApp.execute_as_loop() 0 12 1
A NApp.uri() 0 12 3
A KytosNApp.__init__() 0 40 4

How to fix   Complexity   

Complexity

Complex classes like kytos.core.napps.base often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
"""Kytos Napps Module."""
2 1
import json
3 1
import os
4 1
import re
5 1
import sys
6 1
import tarfile
7 1
import urllib
8 1
from abc import ABCMeta, abstractmethod
9 1
from pathlib import Path
10 1
from random import randint
11 1
from threading import Event, Thread
12
13 1
from kytos.core.events import KytosEvent
14 1
from kytos.core.logs import NAppLog
15
16 1
__all__ = ('KytosNApp',)
17
18 1
LOG = NAppLog()
19
20
21 1
class NApp:
22
    """Class to represent a NApp."""
23
24
    # pylint: disable=too-many-arguments
25 1
    def __init__(self, username=None, name=None, version=None,
26
                 repository=None, meta=False):
27 1
        self.username = username
28 1
        self.name = name
29 1
        self.version = version if version else 'latest'
30 1
        self.repository = repository
31 1
        self.meta = meta
32 1
        self.description = None
33 1
        self.tags = []
34 1
        self.enabled = False
35 1
        self.napp_dependencies = []
36
37 1
    def __str__(self):
38 1
        return "{}/{}".format(self.username, self.name)
39
40 1
    def __repr__(self):
41 1
        return f"NApp({self.username}/{self.name})"
42
43 1
    def __hash__(self):
44
        return hash(self.id)
45
46 1
    def __eq__(self, other):
47
        """Compare username/name strings."""
48
        return isinstance(other, self.__class__) and self.id == other.id
49
50 1
    @property
51
    def id(self):  # pylint: disable=invalid-name
52
        """username/name string."""
53 1
        return str(self)
54
55 1
    @property
56
    def uri(self):
57
        """Return a unique identifier of this NApp."""
58 1
        version = self.version if self.version else 'latest'
59
60 1
        if not self._has_valid_repository():
61 1
            return ""
62
63
        # Use the next line after Diraol fix redirect using ":" for version
64
        # return "{}/{}:{}".format(self.repository, self.id, version)
65
66 1
        return "{}/{}-{}".format(self.repository, self.id, version)
67
68 1
    @property
69
    def package_url(self):
70
        """Return a fully qualified URL for a NApp package."""
71 1
        if not self.uri:
72 1
            return ""
73 1
        return "{}.napp".format(self.uri)
74
75 1
    @classmethod
76
    def create_from_uri(cls, uri):
77
        """Return a new NApp instance from an unique identifier."""
78 1
        regex = r'^(((https?://|file://)(.+))/)?(.+?)/(.+?)/?(:(.+))?$'
79 1
        match = re.match(regex, uri)
80
81 1
        if not match:
82 1
            return None
83
84 1
        return cls(username=match.groups()[4],
85
                   name=match.groups()[5],
86
                   version=match.groups()[7],
87
                   repository=match.groups()[1])
88
89 1
    @classmethod
90
    def create_from_json(cls, filename):
91
        """Return a new NApp instance from a metadata file."""
92 1
        with open(filename, encoding='utf-8') as data_file:
93 1
            data = json.loads(data_file.read())
94
95 1
        return cls.create_from_dict(data)
96
97 1
    @classmethod
98
    def create_from_dict(cls, data):
99
        """Return a new NApp instance from metadata."""
100 1
        napp = cls()
101
102 1
        for attribute, value in data.items():
103 1
            setattr(napp, attribute, value)
104
105 1
        return napp
106
107 1
    def as_json(self):
108
        """Dump all NApp attributes on a json format."""
109
        return json.dumps(self.__dict__)
110
111 1
    def match(self, pattern):
112
        """Whether a pattern is present on NApp id, description and tags."""
113 1
        try:
114 1
            pattern = '.*{}.*'.format(pattern)
115 1
            pattern = re.compile(pattern, re.IGNORECASE)
116 1
            strings = [self.id, self.description] + self.tags
117 1
            return any(pattern.match(string) for string in strings)
118
        except TypeError:
119
            return False
120
121 1
    def download(self):
122
        """Download NApp package from his repository.
123
124
        Raises:
125
            urllib.error.HTTPError: If download is not successful.
126
127
        Returns:
128
            str: Downloaded temp filename.
129
130
        """
131 1
        if not self.package_url:
132 1
            return None
133
134 1
        package_filename = urllib.request.urlretrieve(self.package_url)[0]
135 1
        extracted = self._extract(package_filename)
136 1
        Path(package_filename).unlink()
137 1
        self._update_repo_file(extracted)
138 1
        return extracted
139
140 1
    @staticmethod
141
    def _extract(filename):
142
        """Extract NApp package to a temporary folder.
143
144
        Return:
145
            pathlib.Path: Temp dir with package contents.
146
        """
147 1
        random_string = '{:0d}'.format(randint(0, 10**6))
148 1
        tmp = '/tmp/kytos-napp-' + Path(filename).stem + '-' + random_string
149 1
        os.mkdir(tmp)
150 1
        with tarfile.open(filename, 'r:xz') as tar:
151 1
            tar.extractall(tmp)
152 1
        return Path(tmp)
153
154 1
    def _has_valid_repository(self):
155
        """Whether this NApp has a valid repository or not."""
156 1
        return all([self.username, self.name, self.repository])
157
158 1
    def _update_repo_file(self, destination=None):
159
        """Create or update the file '.repo' inside NApp package."""
160 1
        with open("{}/.repo".format(destination), 'w') as repo_file:
161 1
            repo_file.write(self.repository + '\n')
162
163
164 1
class KytosNApp(Thread, metaclass=ABCMeta):
165
    """Base class for any KytosNApp to be developed."""
166
167 1
    def __init__(self, controller, **kwargs):
168
        """Contructor of KytosNapps.
169
170
        Go through all of the instance methods and selects those that have
171
        the events attribute, then creates a dict containing the event_name
172
        and the list of methods that are responsible for handling such event.
173
174
        At the end, the setup method is called as a complement of the init
175
        process.
176
        """
177 1
        Thread.__init__(self, daemon=False)
178 1
        self.controller = controller
179 1
        self.username = None  # loaded from json
180 1
        self.name = None      # loaded from json
181 1
        self.meta = False     # loaded from json
182 1
        self._load_json()
183
184
        # Force a listener with a private method.
185 1
        self._listeners = {
186
            'kytos/core.shutdown': [self._shutdown_handler],
187
            'kytos/core.shutdown.' + self.napp_id: [self._shutdown_handler]}
188
189 1
        self.__event = Event()
190
        #: int: Seconds to sleep before next call to :meth:`execute`. If
191
        #: negative, run :meth:`execute` only once.
192 1
        self.__interval = -1
193 1
        self.setup()
194
195
        #: Add non-private methods that listen to events.
196 1
        handler_methods = [getattr(self, method_name) for method_name in
197
                           dir(self) if method_name[0] != '_' and
198
                           callable(getattr(self, method_name)) and
199
                           hasattr(getattr(self, method_name), 'events')]
200
201
        # Building the listeners dictionary
202 1
        for method in handler_methods:
203 1
            for event_name in method.events:
204
                if event_name not in self._listeners:
205
                    self._listeners[event_name] = []
206
                self._listeners[event_name].append(method)
207
208 1
    @property
209
    def napp_id(self):
210
        """username/name string."""
211 1
        return "{}/{}".format(self.username, self.name)
212
213 1
    def listeners(self):
214
        """Return all listeners registered."""
215
        return list(self._listeners.keys())
216
217 1
    def _load_json(self):
218
        """Update object attributes based on kytos.json."""
219 1
        current_file = sys.modules[self.__class__.__module__].__file__
220 1
        json_path = os.path.join(os.path.dirname(current_file), 'kytos.json')
221 1
        with open(json_path, encoding='utf-8') as data_file:
222 1
            data = json.loads(data_file.read())
223
224 1
        for attribute, value in data.items():
225 1
            setattr(self, attribute, value)
226
227 1
    def execute_as_loop(self, interval):
228
        """Run :meth:`execute` within a loop. Call this method during setup.
229
230
        By calling this method, the application does not need to worry about
231
        loop details such as sleeping and stopping the loop when
232
        :meth:`shutdown` is called. Just call this method during :meth:`setup`
233
        and implement :meth:`execute` as a single execution.
234
235
        Args:
236
            interval (int): Seconds between each call to :meth:`execute`.
237
        """
238 1
        self.__interval = interval
239
240 1
    def run(self):
241
        """Call the execute method, looping as needed.
242
243
        It should not be overriden.
244
        """
245 1
        self.notify_loaded()
246 1
        LOG.info("Running NApp: %s", self)
247 1
        self.execute()
248 1
        while self.__interval > 0 and not self.__event.is_set():
249 1
            self.__event.wait(self.__interval)
250 1
            self.execute()
251
252 1
    def notify_loaded(self):
253
        """Inform this NApp has been loaded."""
254 1
        name = f'{self.username}/{self.name}.loaded'
255 1
        event = KytosEvent(name=name, content={})
256 1
        self.controller.buffers.app.put(event)
257
258
    # all listeners receive event
259 1
    def _shutdown_handler(self, event):  # pylint: disable=unused-argument
260
        """Listen shutdown event from kytos.
261
262
        This method listens the kytos/core.shutdown event and call the shutdown
263
        method from napp subclass implementation.
264
265
        Paramters
266
            event (:class:`KytosEvent`): event to be listened.
267
        """
268 1
        if not self.__event.is_set():
269 1
            self.__event.set()
270 1
            self.shutdown()
271
272 1
    @abstractmethod
273
    def setup(self):
274
        """Replace the 'init' method for the KytosApp subclass.
275
276
        The setup method is automatically called on the NApp __init__().
277
        Users aren't supposed to call this method directly.
278
        """
279
280 1
    @abstractmethod
281
    def execute(self):
282
        """Execute in a loop until 'kytos/core.shutdown' is received.
283
284
        The execute method is called by KytosNApp class.
285
        Users shouldn't call this method directly.
286
        """
287
288 1
    @abstractmethod
289
    def shutdown(self):
290
        """Run before the app is unloaded and the controller, stopped.
291
292
        The user implementation of this method should do the necessary routine
293
        for the user App and also it is a good moment to break the loop of the
294
        execute method if it is in a loop.
295
296
        This methods is not going to be called directly, it is going to be
297
        called by the _shutdown_handler method when a KytosShutdownEvent is
298
        sent.
299
        """
300