|
1 | 1 | """Provides a service which encapsulates the configuration and execution of individual analytics extensions. |
2 | 2 | Runs an HTTP server which executes the individual extension's analytics function and serves an extension |
3 | 3 | discovery endpoint.""" |
| 4 | +import inspect |
4 | 5 | import json |
5 | | -import errno |
6 | | -import sys |
7 | 6 | from typing import Optional |
8 | 7 |
|
9 | 8 | from flask import Flask, Response |
@@ -44,24 +43,24 @@ def add_analytics_extension(self, analytics_extension: CadenzaAnalyticsExtension |
44 | 43 | ---------- |
45 | 44 | analytics_extension : CadenzaAnalyticsExtension |
46 | 45 | The analytics extension to be added. |
47 | | - """ |
48 | 46 |
|
| 47 | + Raises |
| 48 | + ------ |
| 49 | + ValueError |
| 50 | + If the relative path is already in use or the analytics function has an invalid signature. |
| 51 | + """ |
49 | 52 | self.logger.info('Registering extension "%s" on relative path "%s"...', |
50 | 53 | analytics_extension.print_name, |
51 | 54 | analytics_extension.relative_path |
52 | 55 | ) |
53 | 56 |
|
54 | 57 | # perform some validation checks |
55 | 58 | if analytics_extension.relative_path in [x.relative_path for x in self._analytics_extensions]: |
56 | | - self.logger.critical('Relative path "%s" is already in use by another extension. Exiting...', |
57 | | - analytics_extension.relative_path) |
58 | | - sys.exit(errno.EINTR) |
59 | | - if analytics_extension._analytics_function.__code__.co_argcount != 1: # pylint: disable=W0212 |
60 | | - self.logger.critical('The analytics function "%s()" takes 1 positional arguments, but %s given. Exiting...', |
61 | | - analytics_extension._analytics_function.__name__, # pylint: disable=W0212 |
62 | | - analytics_extension._analytics_function.__code__.co_argcount # pylint: disable=W0212 |
63 | | - ) |
64 | | - sys.exit(errno.EINTR) |
| 59 | + msg = f'Relative path "{analytics_extension.relative_path}" is already in use by another extension.' |
| 60 | + self.logger.critical(msg) |
| 61 | + raise ValueError(msg) |
| 62 | + |
| 63 | + self._validate_analytics_function(analytics_extension._analytics_function) # pylint: disable=W0212 |
65 | 64 |
|
66 | 65 | self._analytics_extensions.append(analytics_extension) |
67 | 66 |
|
@@ -98,6 +97,30 @@ def app(self) -> Flask: |
98 | 97 | """ |
99 | 98 | return self._app |
100 | 99 |
|
| 100 | + def _validate_analytics_function(self, func) -> None: |
| 101 | + """Validate that analytics function has correct signature. |
| 102 | +
|
| 103 | + Parameters |
| 104 | + ---------- |
| 105 | + func : Callable |
| 106 | + The analytics function to validate. |
| 107 | +
|
| 108 | + Raises |
| 109 | + ------ |
| 110 | + ValueError |
| 111 | + If the function does not accept exactly 1 positional argument. |
| 112 | + """ |
| 113 | + try: |
| 114 | + sig = inspect.signature(func) |
| 115 | + params = [p for p in sig.parameters.values() |
| 116 | + if p.kind in (inspect.Parameter.POSITIONAL_ONLY, |
| 117 | + inspect.Parameter.POSITIONAL_OR_KEYWORD)] |
| 118 | + if len(params) != 1: |
| 119 | + raise ValueError(f"Function must accept exactly 1 positional argument, got {len(params)}") |
| 120 | + except (ValueError, TypeError) as err: |
| 121 | + self.logger.critical("Invalid analytics function: %s", err) |
| 122 | + raise ValueError(str(err)) from err |
| 123 | + |
101 | 124 | def _list_extensions(self) -> Response: |
102 | 125 | """List all registered analytics extensions. |
103 | 126 |
|
|
0 commit comments