kytos.core.api_server   F
last analyzed

Complexity

Total Complexity 88

Size/Duplication

Total Lines 608
Duplicated Lines 7.24 %

Importance

Changes 0
Metric Value
eloc 375
dl 44
loc 608
rs 2
c 0
b 0
f 0
wmc 88

34 Methods

Rating   Name   Duplication   Size   Complexity  
A APIServer.start_api() 0 11 1
A APIServer.start_web_ui() 0 11 1
A APIServer.serve() 0 3 1
A APIServer._get_next_route_index() 0 11 1
A APIServer.__init__() 0 45 2
A APIServer.stop() 0 3 1
A APIServer.register_rest_endpoint() 0 8 2
A APIServer.start_web_ui_static_files() 0 4 1
A APIServer._http_exc_handler() 0 9 1
A APIServer.register_core_endpoint() 0 7 1
A APIServer.status_api() 0 9 2
A APIServer.register_core_napp_services() 0 22 1
A APIServer._unzip_backup_web_ui() 0 20 4
A APIServer._list_enabled_napps() 0 5 1
B APIServer._get_decorated_functions() 0 19 8
A APIServer._fetch_latest_ui_tag() 0 13 2
A APIServer.register_napp_endpoints() 0 16 4
A APIServer._install_napp() 0 21 4
A APIServer.web_ui() 0 7 2
B APIServer.authenticate_endpoints() 0 18 6
A APIServer.get_authenticate_options() 0 5 1
A APIServer._add_non_strict_slash_route() 0 13 3
A APIServer._list_installed_napps() 0 5 1
A APIServer._start_endpoint() 0 12 1
A APIServer._get_napp_metadata() 0 22 3
A APIServer.get_ui_components() 0 26 3
A APIServer.get_absolute_rule() 0 10 2
A APIServer._uninstall_napp() 0 13 3
A APIServer.decorate_as_endpoint() 0 44 3
A APIServer._enable_napp() 22 22 4
A APIServer._disable_napp() 22 22 4
A APIServer.remove_napp_endpoints() 0 14 4
B APIServer.update_web_ui() 0 44 8
A APIServer.static_web_ui() 0 15 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like kytos.core.api_server 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
"""Module used to handle a API Server."""
2
import logging
3
import os
4
import shutil
5
import warnings
6
import zipfile
7
from datetime import datetime
8
from glob import glob
9
from http import HTTPStatus
10
from typing import Optional, Union
11
from urllib.error import HTTPError, URLError
12
from urllib.request import urlretrieve
13
14
import httpx
15
from anyio import CapacityLimiter
16
from anyio.lowlevel import RunVar
17
from starlette.applications import Starlette
18
from starlette.exceptions import HTTPException
19
from starlette.middleware import Middleware
20
from starlette.middleware.cors import CORSMiddleware
21
from starlette.responses import FileResponse
22
from starlette.staticfiles import StaticFiles
23
from uvicorn import Config as UvicornConfig
24
from uvicorn import Server
25
26
from kytos.core.auth import authenticated
27
from kytos.core.config import KytosConfig
28
from kytos.core.helpers import get_thread_pool_max_workers
29
from kytos.core.rest_api import JSONResponse, Request
30
31
LOG = logging.getLogger(__name__)
32
33
34
class APIServer:
35
    """Api server used to provide Kytos Controller routes."""
36
37
    DEFAULT_METHODS = ('GET',)
38
    _NAPP_PREFIX = "/api/{napp.username}/{napp.name}/"
39
    _CORE_PREFIX = "/api/kytos/core/"
40
    _route_index_count = 0
41
42
    # pylint: disable=too-many-arguments
43
    def __init__(self, listen='0.0.0.0', port=8181,
44
                 napps_manager=None, napps_dir=None,
45
                 response_traceback_on_500=True):
46
        """APIServer.
47
48
        Require controller to get NApps dir and NAppsManager
49
        """
50
        dirname = os.path.dirname(os.path.abspath(__file__))
51
        self.napps_manager = napps_manager
52
        self.napps_dir = napps_dir
53
        self.listen = listen
54
        self.port = port
55
        self.web_ui_dir = os.path.join(dirname, '../web-ui')
56
        self.app = Starlette(
57
            debug=response_traceback_on_500,
58
            exception_handlers={HTTPException: self._http_exc_handler},
59
            middleware=[
60
                Middleware(CORSMiddleware, allow_origins=["*"]),
61
            ],
62
        )
63
        kytos_conf = KytosConfig().options["daemon"]
64
65
        api_threadpool_size = get_thread_pool_max_workers().get('api', 512)
66
67
        concurrency_limit = kytos_conf.api_concurrency_limit
68
        if concurrency_limit == 'threadpool':
69
            concurrency_limit = api_threadpool_size
70
71
        @self.app.on_event("startup")
72
        def _app_startup_func():
73
            RunVar("_default_thread_limiter").set(
74
                CapacityLimiter(api_threadpool_size)
75
            )
76
77
        self.server = Server(
78
            UvicornConfig(
79
                self.app,
80
                host=self.listen,
81
                port=self.port,
82
                limit_concurrency=concurrency_limit
83
            )
84
        )
85
86
        # Update web-ui if necessary
87
        self.update_web_ui(None, force=False)
88
89
    async def _http_exc_handler(self, _request: Request, exc: HTTPException):
90
        """HTTPException handler.
91
92
        The response format is still partly compatible with how werkzeug used
93
        to format http exceptions.
94
        """
95
        return JSONResponse({"description": exc.detail,
96
                             "code": exc.status_code},
97
                            status_code=exc.status_code)
98
99
    @classmethod
100
    def _get_next_route_index(cls) -> int:
101
        """Get next route index.
102
103
        This classmethod is meant to ensure route ordering when allocating
104
        an index for each route, the @rest decorator will use it. Decorated
105
        routes are imported sequentially so this won't need a threading Lock.
106
        """
107
        index = cls._route_index_count
108
        cls._route_index_count += 1
109
        return index
110
111
    async def serve(self):
112
        """Start serving."""
113
        await self.server.serve()
114
115
    def stop(self):
116
        """Stop serving."""
117
        self.server.should_exit = True
118
119
    def register_rest_endpoint(self, url, function, methods):
120
        """Deprecate in favor of @rest decorator."""
121
        warnings.warn("From now on, use @rest decorator.", DeprecationWarning,
122
                      stacklevel=2)
123
        if url.startswith('/'):
124
            url = url[1:]
125
        self._start_endpoint(self.app, f'/kytos/{url}', function,
126
                             methods=methods)
127
128
    def start_api(self):
129
        """Start this APIServer instance API."""
130
        self.register_core_endpoint('status/', self.status_api)
131
        self.register_core_endpoint('web/update/{version}/',
132
                                    self.update_web_ui,
133
                                    methods=['POST'])
134
        self.register_core_endpoint('web/update/',
135
                                    self.update_web_ui,
136
                                    methods=['POST'])
137
138
        self.register_core_napp_services()
139
140
    def start_web_ui(self) -> None:
141
        """Start Web UI endpoints."""
142
        self._start_endpoint(self.app,
143
                             "/ui/{username}/{napp_name}/{filename:path}",
144
                             self.static_web_ui)
145
        self._start_endpoint(self.app,
146
                             "/ui/{section_name}/",
147
                             self.get_ui_components)
148
        self._start_endpoint(self.app, '/', self.web_ui)
149
        self._start_endpoint(self.app, '/index.html', self.web_ui)
150
        self.start_web_ui_static_files()
151
152
    def start_web_ui_static_files(self) -> None:
153
        """Start Web UI static files."""
154
        self.app.router.mount("/", app=StaticFiles(directory=self.web_ui_dir),
155
                              name="dist")
156
157
    def register_core_endpoint(self, route, function, **options):
158
        """Register an endpoint with the URL /api/kytos/core/<route>.
159
160
        Not used by NApps, but controller.
161
        """
162
        self._start_endpoint(self.app, f"{self._CORE_PREFIX}{route}",
163
                             function, **options)
164
165
    def status_api(self, _request: Request):
166
        """Display kytos status using the route ``/kytos/status/``."""
167
        uptime = self.napps_manager._controller.uptime()
168
        response = {
169
            "response": "running",
170
            "started_at": self.napps_manager._controller.started_at,
171
            "uptime_seconds": uptime.seconds if uptime else 0,
172
        }
173
        return JSONResponse(response)
174
175
    def static_web_ui(self,
176
                      request: Request) -> Union[FileResponse, JSONResponse]:
177
        """Serve static files from installed napps."""
178
        username = request.path_params["username"]
179
        napp_name = request.path_params["napp_name"]
180
        filename = request.path_params["filename"]
181
        path = f"{self.napps_dir}/{username}/{napp_name}/ui/{filename}"
182
        if os.path.exists(path):
183
            headers = {
184
                "Cache-Control": "no-cache, no-store, must-revalidate",
185
                "Pragma": "no-cache",
186
                "Expires": "0"
187
            }
188
            return FileResponse(path, headers=headers)
189
        return JSONResponse("", status_code=HTTPStatus.NOT_FOUND.value)
190
191
    def get_ui_components(self, request: Request) -> JSONResponse:
192
        """Return all napps ui components from an specific section.
193
194
        The component name generated will have the following structure:
195
        {username}-{nappname}-{component-section}-{filename}`
196
197
        Args:
198
            section_name (str): Specific section name
199
200
        Returns:
201
            str: Json with a list of all components found.
202
203
        """
204
        section_name = request.path_params["section_name"]
205
        section_name = '*' if section_name == "all" else section_name
206
        path = f"{self.napps_dir}/*/*/ui/{section_name}/*.kytos"
207
        components = []
208
        for name in glob(path):
209
            dirs_name = name.split('/')
210
            dirs_name.remove('ui')
211
212
            component_name = '-'.join(dirs_name[-4:]).replace('.kytos', '')
213
            url = f'ui/{"/".join(dirs_name[-4:])}'
214
            component = {'name': component_name, 'url': url}
215
            components.append(component)
216
        return JSONResponse(components)
217
218
    def web_ui(self, _request: Request) -> Union[JSONResponse, FileResponse]:
219
        """Serve the index.html page for the admin-ui."""
220
        index_path = f"{self.web_ui_dir}/index.html"
221
        if os.path.exists(index_path):
222
            return FileResponse(index_path)
223
        return JSONResponse(f"File '{index_path}' not found.",
224
                            status_code=HTTPStatus.NOT_FOUND.value)
225
226
    def _unzip_backup_web_ui(self, package: str, uri: str) -> None:
227
        """Unzip and backup web ui files.
228
229
        backup the old web-ui files and create a new web-ui folder
230
        if there is no path to backup, zip.extractall will
231
        create the path.
232
        """
233
        with zipfile.ZipFile(package, 'r') as zip_ref:
234
            if zip_ref.testzip() is not None:
235
                LOG.error("Web update - Zip file from %s "
236
                          "is corrupted.", uri)
237
                raise ValueError(f'Zip file from {uri} is corrupted.')
238
            if os.path.exists(self.web_ui_dir):
239
                LOG.info("Web update - Performing UI backup.")
240
                date = datetime.now().strftime("%Y%m%d%H%M%S")
241
                shutil.move(self.web_ui_dir, f"{self.web_ui_dir}-{date}")
242
                os.mkdir(self.web_ui_dir)
243
            # unzip and extract files to web-ui/*
244
            zip_ref.extractall(self.web_ui_dir)
245
            zip_ref.close()
246
247
    def _fetch_latest_ui_tag(self) -> str:
248
        """Fetch latest ui tag version from GitHub."""
249
        version = '2022.3.0'
250
        try:
251
            url = ('https://api.github.com/repos/kytos-ng/'
252
                   'ui/releases/latest')
253
            response = httpx.get(url, timeout=10)
254
            version = response.json()['tag_name']
255
        except (httpx.RequestError, KeyError):
256
            msg = "Failed to fetch latest tag from GitHub, " \
257
                  f"falling back to {version}"
258
            LOG.warning(f"Web update - {msg}")
259
        return version
260
261
    def update_web_ui(self, request: Optional[Request],
262
                      version='latest',
263
                      force=True) -> JSONResponse:
264
        """Update the static files for the Web UI.
265
266
        Download the latest files from the UI github repository and update them
267
        in the ui folder.
268
        The repository link is currently hardcoded here.
269
        """
270
        if not os.path.exists(self.web_ui_dir) or force:
271
            if request:
272
                version = request.path_params.get("version", "latest")
273
            if version == 'latest':
274
                version = self._fetch_latest_ui_tag()
275
276
            repository = "https://github.com/kytos-ng/ui"
277
            uri = repository + f"/releases/download/{version}/latest.zip"
278
            try:
279
                LOG.info("Web update - Downloading UI from %s.", uri)
280
                package = urlretrieve(uri)[0]
281
                self._unzip_backup_web_ui(package, uri)
282
            except HTTPError:
283
                LOG.error("Web update - Uri not found %s.", uri)
284
                return JSONResponse(
285
                        f"Uri not found {uri}.",
286
                        status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value
287
                        )
288
            except URLError:
289
                LOG.error("Web update - Error accessing URL %s.", uri)
290
                return JSONResponse(
291
                        f"Error accessing URL {uri}.",
292
                        status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value
293
                       )
294
            except ValueError as exc:
295
                return JSONResponse(
296
                        str(exc),
297
                        status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value
298
                       )
299
            LOG.info("Web update - Updated")
300
            return JSONResponse("Web UI was updated")
301
302
        LOG.error("Web update - Web UI was not updated")
303
        return JSONResponse("Web ui was not updated",
304
                            status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value)
305
306
    # BEGIN decorator methods
307
    @staticmethod
308
    def decorate_as_endpoint(rule, **options):
309
        """Decorate methods as REST endpoints.
310
311
        Example for URL ``/api/myusername/mynapp/sayhello/World``:
312
313
        .. code-block:: python3
314
315
316
           from kytos.core.napps import rest
317
           from kytos.core.rest_api import JSONResponse, Request
318
319
           @rest("sayhello/{name}")
320
           def say_hello(request: Request) -> JSONResponse:
321
               name = request.path_params["name"]
322
               return JSONResponse({"data": f"Hello, {name}!"})
323
324
        ``@rest`` parameters are the same as Starlette's ``add_route``. You can
325
        also add ``methods=['POST']``, for example.
326
327
        As we don't have the NApp instance now, we store the parameters in a
328
        method attribute in order to add the route later, after we have both
329
        APIServer and NApp instances.
330
        """
331
        def store_route_params(function):
332
            """Store ``@route`` parameters in a method attribute.
333
334
            There can be many @route decorators in a single function.
335
            """
336
            # To support any order: @classmethod, @rest or @rest, @classmethod
337
            # class and static decorators return a descriptor with the function
338
            # in __func__.
339
            if isinstance(function, (classmethod, staticmethod)):
340
                inner = function.__func__
341
            else:
342
                inner = function
343
            # Add route parameters
344
            if not hasattr(inner, 'route_params'):
345
                inner.route_params = []
346
            inner.route_params.append((rule, options))
347
            inner.route_index = APIServer._get_next_route_index()
348
            # Return the same function, now with "route_params" attribute
349
            return function
350
        return store_route_params
351
352
    @staticmethod
353
    def get_authenticate_options():
354
        """Return configuration options related to authentication."""
355
        options = KytosConfig().options['daemon']
356
        return options.authenticate_urls
357
358
    def authenticate_endpoints(self, napp):
359
        """Add authentication to defined REST endpoints.
360
361
        If any URL marked for authentication uses a function,
362
        that function will require authentication.
363
        """
364
        authenticate_urls = self.get_authenticate_options()
365
        for function in self._get_decorated_functions(napp):
366
            inner = getattr(function, '__func__', function)
367
            inner.authenticated = False
368
            for rule, _ in function.route_params:
369
                if inner.authenticated:
370
                    break
371
                absolute_rule = self.get_absolute_rule(rule, napp)
372
                for url in authenticate_urls:
373
                    if url in absolute_rule:
374
                        inner.authenticated = True
375
                        break
376
377
    def register_napp_endpoints(self, napp):
378
        """Add all NApp REST endpoints with @rest decorator.
379
380
        URLs will be prefixed with ``/api/{username}/{napp_name}/``.
381
382
        Args:
383
            napp (Napp): Napp instance to register new endpoints.
384
        """
385
        # Start all endpoints for this NApp
386
        for function in self._get_decorated_functions(napp):
387
            for rule, options in function.route_params:
388
                absolute_rule = self.get_absolute_rule(rule, napp)
389
                if getattr(function, 'authenticated', False):
390
                    function = authenticated(function)
391
                self._start_endpoint(self.app, absolute_rule,
392
                                     function, **options)
393
394
    @staticmethod
395
    def _get_decorated_functions(napp):
396
        """Return ``napp``'s methods having the @rest decorator.
397
398
        The callables are yielded based on their decorated order,
399
        this ensures deterministic routing matching order.
400
        """
401
        callables = []
402
        for name in dir(napp):
403
            if not name.startswith('_'):  # discarding private names
404
                pub_attr = getattr(napp, name)
405
                if (
406
                    callable(pub_attr)
407
                    and hasattr(pub_attr, "route_params")
408
                    and hasattr(pub_attr, "route_index")
409
                    and isinstance(pub_attr.route_index, int)
410
                ):
411
                    callables.append(pub_attr)
412
        yield from sorted(callables, key=lambda f: f.route_index)
413
414
    @classmethod
415
    def get_absolute_rule(cls, rule, napp):
416
        """Prefix the rule, e.g. "flow" to "/api/user/napp/flow".
417
418
        This code is used by kytos-utils when generating an OpenAPI skel.
419
        """
420
        # Flask does require 2 slashes if specified, so we remove a starting
421
        # slash if applicable.
422
        relative_rule = rule[1:] if rule.startswith('/') else rule
423
        return cls._NAPP_PREFIX.format(napp=napp) + relative_rule
424
425
    # END decorator methods
426
427
    def _add_non_strict_slash_route(
428
        self,
429
        app: Starlette,
430
        route: str,
431
        function,
432
        **options
433
    ) -> None:
434
        """Try to add a non strict slash route."""
435
        if route == "/":
436
            return
437
        non_strict = route[:-1] if route.endswith("/") else f"{route}/"
438
        app.router.add_route(non_strict, function, **options,
439
                             include_in_schema=False)
440
441
    def _start_endpoint(
442
        self,
443
        app: Starlette,
444
        route: str,
445
        function,
446
        **options
447
    ):
448
        """Start ``function``'s endpoint."""
449
        app.router.add_route(route, function, **options)
450
        self._add_non_strict_slash_route(app, route, function, **options)
451
        LOG.info('Started %s - %s', route,
452
                 ', '.join(options.get('methods', self.DEFAULT_METHODS)))
453
454
    def remove_napp_endpoints(self, napp):
455
        """Remove all decorated endpoints.
456
457
        Args:
458
            napp (Napp): Napp instance to look for rest-decorated methods.
459
        """
460
        prefix = self._NAPP_PREFIX.format(napp=napp)
461
        indexes = []
462
        for index, route in enumerate(self.app.routes):
463
            if route.path.startswith(prefix):
464
                indexes.append(index)
465
        for index in reversed(indexes):
466
            self.app.routes.pop(index)
467
        LOG.info('The Rest endpoints from %s were disabled.', prefix)
468
469
    def register_core_napp_services(self):
470
        """
471
        Register /kytos/core/ services over NApps.
472
473
        It registers enable, disable, install, uninstall NApps that will
474
        be used by kytos-utils.
475
        """
476
        self.register_core_endpoint("napps/{username}/{napp_name}/enable",
477
                                    self._enable_napp)
478
        self.register_core_endpoint("napps/{username}/{napp_name}/disable",
479
                                    self._disable_napp)
480
        self.register_core_endpoint("napps/{username}/{napp_name}/install",
481
                                    self._install_napp)
482
        self.register_core_endpoint("napps/{username}/{napp_name}/uninstall",
483
                                    self._uninstall_napp)
484
        self.register_core_endpoint("napps_enabled",
485
                                    self._list_enabled_napps)
486
        self.register_core_endpoint("napps_installed",
487
                                    self._list_installed_napps)
488
        self.register_core_endpoint(
489
            "napps/{username}/{napp_name}/metadata/{key}",
490
            self._get_napp_metadata)
491
492 View Code Duplication
    def _enable_napp(self, request: Request) -> JSONResponse:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
493
        """Enable an installed NApp."""
494
495
        username = request.path_params["username"]
496
        napp_name = request.path_params["napp_name"]
497
        # Check if the NApp is installed
498
        if not self.napps_manager.is_installed(username, napp_name):
499
            return JSONResponse({"response": "not installed"},
500
                                status_code=HTTPStatus.BAD_REQUEST.value)
501
502
        # Check if the NApp is already been enabled
503
        if not self.napps_manager.is_enabled(username, napp_name):
504
            self.napps_manager.enable(username, napp_name)
505
506
        # Check if NApp is enabled
507
        if not self.napps_manager.is_enabled(username, napp_name):
508
            # If it is not enabled an admin user must check the log file
509
            status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
510
            return JSONResponse({"response": "error"},
511
                                status_code=status_code)
512
513
        return JSONResponse({"response": "enabled"})
514
515 View Code Duplication
    def _disable_napp(self, request: Request) -> JSONResponse:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
516
        """Disable an installed NApp."""
517
518
        username = request.path_params["username"]
519
        napp_name = request.path_params["napp_name"]
520
        # Check if the NApp is installed
521
        if not self.napps_manager.is_installed(username, napp_name):
522
            return JSONResponse({"response": "not installed"},
523
                                status_code=HTTPStatus.BAD_REQUEST.value)
524
525
        # Check if the NApp is enabled
526
        if self.napps_manager.is_enabled(username, napp_name):
527
            self.napps_manager.disable(username, napp_name)
528
529
        # Check if NApp is still enabled
530
        if self.napps_manager.is_enabled(username, napp_name):
531
            # If it is still enabled an admin user must check the log file
532
            status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
533
            return JSONResponse({"response": "error"},
534
                                status_code=status_code)
535
536
        return JSONResponse({"response": "disabled"})
537
538
    def _install_napp(self, request: Request) -> JSONResponse:
539
        username = request.path_params["username"]
540
        napp_name = request.path_params["napp_name"]
541
        # Check if the NApp is installed
542
        if self.napps_manager.is_installed(username, napp_name):
543
            return JSONResponse({"response": "installed"})
544
545
        napp = f"{username}/{napp_name}"
546
547
        # Try to install and enable the napp
548
        try:
549
            if not self.napps_manager.install(napp, enable=True):
550
                # If it is not installed an admin user must check the log file
551
                status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
552
                return JSONResponse({"response": "error"},
553
                                    status_code=status_code)
554
        except HTTPError as exception:
555
            return JSONResponse({"response": "error"},
556
                                status_code=exception.code)
557
558
        return JSONResponse({"response": "installed"})
559
560
    def _uninstall_napp(self, request: Request) -> JSONResponse:
561
        username = request.path_params["username"]
562
        napp_name = request.path_params["napp_name"]
563
        # Check if the NApp is installed
564
        if self.napps_manager.is_installed(username, napp_name):
565
            # Try to unload/uninstall the napp
566
            if not self.napps_manager.uninstall(username, napp_name):
567
                # If it is not uninstalled admin user must check the log file
568
                status_code = HTTPStatus.INTERNAL_SERVER_ERROR.value
569
                return JSONResponse({"response": "error"},
570
                                    status_code=status_code)
571
572
        return JSONResponse({"response": "uninstalled"})
573
574
    def _list_enabled_napps(self, _request: Request) -> JSONResponse:
575
        """Sorted list of (username, napp_name) of enabled napps."""
576
        napps = self.napps_manager.get_enabled_napps()
577
        napps = [[n.username, n.name] for n in napps]
578
        return JSONResponse({"napps": napps})
579
580
    def _list_installed_napps(self, _request: Request) -> JSONResponse:
581
        """Sorted list of (username, napp_name) of installed napps."""
582
        napps = self.napps_manager.get_installed_napps()
583
        napps = [[n.username, n.name] for n in napps]
584
        return JSONResponse({"napps": napps})
585
586
    def _get_napp_metadata(self, request: Request) -> JSONResponse:
587
        """Get NApp metadata value.
588
589
        For safety reasons, only some keys can be retrieved:
590
        napp_dependencies, description, version.
591
592
        """
593
        username = request.path_params["username"]
594
        napp_name = request.path_params["napp_name"]
595
        key = request.path_params["key"]
596
        valid_keys = ['napp_dependencies', 'description', 'version']
597
598
        if not self.napps_manager.is_installed(username, napp_name):
599
            return JSONResponse("NApp is not installed.",
600
                                status_code=HTTPStatus.BAD_REQUEST.value)
601
602
        if key not in valid_keys:
603
            return JSONResponse("Invalid key.",
604
                                status_code=HTTPStatus.BAD_REQUEST.value)
605
606
        data = self.napps_manager.get_napp_metadata(username, napp_name, key)
607
        return JSONResponse({key: data})
608