Passed
Push — master ( de8926...d30041 )
by Oleksandr
11:49
created

tabpy.tabpy_server.app.app   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 434
Duplicated Lines 0 %

Test Coverage

Coverage 81.68%

Importance

Changes 0
Metric Value
wmc 52
eloc 290
dl 0
loc 434
ccs 156
cts 191
cp 0.8168
rs 7.44
c 0
b 0
f 0

10 Methods

Rating   Name   Duplication   Size   Complexity  
A TabPyApp._parse_pwd_file() 0 10 3
B TabPyApp._validate_cert_key_state() 0 18 6
B TabPyApp._set_parameter() 0 29 8
A TabPyApp.__init__() 0 14 4
A TabPyApp._validate_transfer_protocol_settings() 0 29 4
D TabPyApp._parse_config() 0 132 11
B TabPyApp._create_tornado_web_app() 0 70 3
A TabPyApp.run() 0 33 3
A TabPyApp._get_features() 0 11 2
A TabPyApp._build_tabpy_state() 0 17 2

1 Function

Rating   Name   Duplication   Size   Complexity  
B _init_asyncio_patch() 0 17 6

How to fix   Complexity   

Complexity

Complex classes like tabpy.tabpy_server.app.app 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 1
import concurrent.futures
2 1
import configparser
3 1
import logging
4 1
import multiprocessing
5 1
import os
6 1
import shutil
7 1
import signal
8 1
import sys
9 1
import tabpy
10 1
from tabpy.tabpy import __version__
11 1
from tabpy.tabpy_server.app.app_parameters import ConfigParameters, SettingsParameters
12 1
from tabpy.tabpy_server.app.util import parse_pwd_file
13 1
from tabpy.tabpy_server.management.state import TabPyState
14 1
from tabpy.tabpy_server.management.util import _get_state_from_file
15 1
from tabpy.tabpy_server.psws.callbacks import init_model_evaluator, init_ps_server
16 1
from tabpy.tabpy_server.psws.python_service import PythonService, PythonServiceHandler
17 1
from tabpy.tabpy_server.handlers import (
18
    EndpointHandler,
19
    EndpointsHandler,
20
    EvaluationPlaneHandler,
21
    QueryPlaneHandler,
22
    ServiceInfoHandler,
23
    StatusHandler,
24
    UploadDestinationHandler,
25
)
26 1
import tornado
27
28
29 1
logger = logging.getLogger(__name__)
30
31
32 1
def _init_asyncio_patch():
33
    """
34
    Select compatible event loop for Tornado 5+.
35
    As of Python 3.8, the default event loop on Windows is `proactor`,
36
    however Tornado requires the old default "selector" event loop.
37
    As Tornado has decided to leave this to users to set, MkDocs needs
38
    to set it. See https://github.com/tornadoweb/tornado/issues/2608.
39
    """
40 1
    if sys.platform.startswith("win") and sys.version_info >= (3, 8):
41
        import asyncio
42
        try:
43
            from asyncio import WindowsSelectorEventLoopPolicy
44
        except ImportError:
45
            pass  # Can't assign a policy which doesn't exist.
46
        else:
47
            if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy):
48
                asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy())
49
50
51 1
class TabPyApp:
52
    """
53
    TabPy application class for keeping context like settings, state, etc.
54
    """
55
56 1
    settings = {}
57 1
    subdirectory = ""
58 1
    tabpy_state = None
59 1
    python_service = None
60 1
    credentials = {}
61
62 1
    def __init__(self, config_file):
63 1
        if config_file is None:
64 1
            config_file = os.path.join(
65
                os.path.dirname(__file__), os.path.pardir, "common", "default.conf"
66
            )
67
68 1
        if os.path.isfile(config_file):
69 1
            try:
70 1
                from logging import config
71 1
                config.fileConfig(config_file, disable_existing_loggers=False)
72 1
            except KeyError:
73 1
                logging.basicConfig(level=logging.DEBUG)
74
75 1
        self._parse_config(config_file)
76
77 1
    def run(self):
78
        application = self._create_tornado_web_app()
79
        max_request_size = (
80
            int(self.settings[SettingsParameters.MaxRequestSizeInMb]) * 1024 * 1024
81
        )
82
        logger.info(f"Setting max request size to {max_request_size} bytes")
83
84
        init_model_evaluator(self.settings, self.tabpy_state, self.python_service)
85
86
        protocol = self.settings[SettingsParameters.TransferProtocol]
87
        ssl_options = None
88
        if protocol == "https":
89
            ssl_options = {
90
                "certfile": self.settings[SettingsParameters.CertificateFile],
91
                "keyfile": self.settings[SettingsParameters.KeyFile],
92
            }
93
        elif protocol != "http":
94
            msg = f"Unsupported transfer protocol {protocol}."
95
            logger.critical(msg)
96
            raise RuntimeError(msg)
97
98
        application.listen(
99
            self.settings[SettingsParameters.Port],
100
            ssl_options=ssl_options,
101
            max_buffer_size=max_request_size,
102
            max_body_size=max_request_size,
103
        )
104
105
        logger.info(
106
            "Web service listening on port "
107
            f"{str(self.settings[SettingsParameters.Port])}"
108
        )
109
        tornado.ioloop.IOLoop.instance().start()
110
111 1
    def _create_tornado_web_app(self):
112 1
        class TabPyTornadoApp(tornado.web.Application):
113 1
            is_closing = False
114
115 1
            def signal_handler(self, signal, _):
116
                logger.critical(f"Exiting on signal {signal}...")
117
                self.is_closing = True
118
119 1
            def try_exit(self):
120
                if self.is_closing:
121
                    tornado.ioloop.IOLoop.instance().stop()
122
                    logger.info("Shutting down TabPy...")
123
124 1
        logger.info("Initializing TabPy...")
125 1
        tornado.ioloop.IOLoop.instance().run_sync(
126
            lambda: init_ps_server(self.settings, self.tabpy_state)
127
        )
128 1
        logger.info("Done initializing TabPy.")
129
130 1
        executor = concurrent.futures.ThreadPoolExecutor(
131
            max_workers=multiprocessing.cpu_count()
132
        )
133
134
        # initialize Tornado application
135 1
        _init_asyncio_patch()
136 1
        application = TabPyTornadoApp(
137
            [
138
                (
139
                    self.subdirectory + r"/query/([^/]+)",
140
                    QueryPlaneHandler,
141
                    dict(app=self),
142
                ),
143
                (self.subdirectory + r"/status", StatusHandler, dict(app=self)),
144
                (self.subdirectory + r"/info", ServiceInfoHandler, dict(app=self)),
145
                (self.subdirectory + r"/endpoints", EndpointsHandler, dict(app=self)),
146
                (
147
                    self.subdirectory + r"/endpoints/([^/]+)?",
148
                    EndpointHandler,
149
                    dict(app=self),
150
                ),
151
                (
152
                    self.subdirectory + r"/evaluate",
153
                    EvaluationPlaneHandler,
154
                    dict(executor=executor, app=self),
155
                ),
156
                (
157
                    self.subdirectory + r"/configurations/endpoint_upload_destination",
158
                    UploadDestinationHandler,
159
                    dict(app=self),
160
                ),
161
                (
162
                    self.subdirectory + r"/(.*)",
163
                    tornado.web.StaticFileHandler,
164
                    dict(
165
                        path=self.settings[SettingsParameters.StaticPath],
166
                        default_filename="index.html",
167
                    ),
168
                ),
169
            ],
170
            debug=False,
171
            **self.settings,
172
        )
173
174 1
        signal.signal(signal.SIGINT, application.signal_handler)
175 1
        tornado.ioloop.PeriodicCallback(application.try_exit, 500).start()
176
177 1
        signal.signal(signal.SIGINT, application.signal_handler)
178 1
        tornado.ioloop.PeriodicCallback(application.try_exit, 500).start()
179
180 1
        return application
181
182 1
    def _set_parameter(self, parser, settings_key, config_key, default_val, parse_function):
183 1
        key_is_set = False
184
185 1
        if (
186
            config_key is not None
187
            and parser.has_section("TabPy")
188
            and parser.has_option("TabPy", config_key)
189
        ):
190 1
            if parse_function is None:
191 1
                parse_function = parser.get
192 1
            self.settings[settings_key] = parse_function("TabPy", config_key)
193 1
            key_is_set = True
194 1
            logger.debug(
195
                f"Parameter {settings_key} set to "
196
                f'"{self.settings[settings_key]}" '
197
                "from config file or environment variable"
198
            )
199
200 1
        if not key_is_set and default_val is not None:
201 1
            self.settings[settings_key] = default_val
202 1
            key_is_set = True
203 1
            logger.debug(
204
                f"Parameter {settings_key} set to "
205
                f'"{self.settings[settings_key]}" '
206
                "from default value"
207
            )
208
209 1
        if not key_is_set:
210 1
            logger.debug(f"Parameter {settings_key} is not set")
211
212 1
    def _parse_config(self, config_file):
213
        """Provide consistent mechanism for pulling in configuration.
214
215
        Attempt to retain backward compatibility for
216
        existing implementations by grabbing port
217
        setting from CLI first.
218
219
        Take settings in the following order:
220
221
        1. CLI arguments if present
222
        2. config file
223
        3. OS environment variables (for ease of
224
           setting defaults if not present)
225
        4. current defaults if a setting is not present in any location
226
227
        Additionally provide similar configuration capabilities in between
228
        config file and environment variables.
229
        For consistency use the same variable name in the config file as
230
        in the os environment.
231
        For naming standards use all capitals and start with 'TABPY_'
232
        """
233 1
        self.settings = {}
234 1
        self.subdirectory = ""
235 1
        self.tabpy_state = None
236 1
        self.python_service = None
237 1
        self.credentials = {}
238
239 1
        pkg_path = os.path.dirname(tabpy.__file__)
240
241 1
        parser = configparser.ConfigParser(os.environ)
242 1
        logger.info(f"Parsing config file {config_file}")
243
244 1
        file_exists = False
245 1
        if os.path.isfile(config_file):
246 1
            try:
247 1
                with open(config_file, 'r') as f:
248 1
                    parser.read_string(f.read())
249 1
                    file_exists = True
250
            except Exception:
251
                pass
252
253 1
        if not file_exists:
254 1
            logger.warning(
255
                f"Unable to open config file {config_file}, "
256
                "using default settings."
257
            )
258
259 1
        settings_parameters = [
260
            (SettingsParameters.Port, ConfigParameters.TABPY_PORT, 9004, None),
261
            (SettingsParameters.ServerVersion, None, __version__, None),
262
            (SettingsParameters.EvaluateTimeout, ConfigParameters.TABPY_EVALUATE_TIMEOUT,
263
             30, parser.getfloat),
264
            (SettingsParameters.UploadDir, ConfigParameters.TABPY_QUERY_OBJECT_PATH,
265
             os.path.join(pkg_path, "tmp", "query_objects"), None),
266
            (SettingsParameters.TransferProtocol, ConfigParameters.TABPY_TRANSFER_PROTOCOL,
267
             "http", None),
268
            (SettingsParameters.CertificateFile, ConfigParameters.TABPY_CERTIFICATE_FILE,
269
             None, None),
270
            (SettingsParameters.KeyFile, ConfigParameters.TABPY_KEY_FILE, None, None),
271
            (SettingsParameters.StateFilePath, ConfigParameters.TABPY_STATE_PATH,
272
             os.path.join(pkg_path, "tabpy_server"), None),
273
            (SettingsParameters.StaticPath, ConfigParameters.TABPY_STATIC_PATH,
274
             os.path.join(pkg_path, "tabpy_server", "static"), None),
275
            (ConfigParameters.TABPY_PWD_FILE, ConfigParameters.TABPY_PWD_FILE, None, None),
276
            (SettingsParameters.LogRequestContext, ConfigParameters.TABPY_LOG_DETAILS,
277
             "false", None),
278
            (SettingsParameters.MaxRequestSizeInMb, ConfigParameters.TABPY_MAX_REQUEST_SIZE_MB,
279
             100, None),
280
        ]
281
282 1
        for setting, parameter, default_val, parse_function in settings_parameters:
283 1
            self._set_parameter(parser, setting, parameter, default_val, parse_function)
284
285 1
        if not os.path.exists(self.settings[SettingsParameters.UploadDir]):
286 1
            os.makedirs(self.settings[SettingsParameters.UploadDir])
287
288
        # set and validate transfer protocol
289 1
        self.settings[SettingsParameters.TransferProtocol] = self.settings[
290
            SettingsParameters.TransferProtocol
291
        ].lower()
292
293 1
        self._validate_transfer_protocol_settings()
294
295
        # if state.ini does not exist try and create it - remove
296
        # last dependence on batch/shell script
297 1
        self.settings[SettingsParameters.StateFilePath] = os.path.realpath(
298
            os.path.normpath(
299
                os.path.expanduser(self.settings[SettingsParameters.StateFilePath])
300
            )
301
        )
302 1
        state_config, self.tabpy_state = self._build_tabpy_state()
303
304 1
        self.python_service = PythonServiceHandler(PythonService())
305 1
        self.settings["compress_response"] = True
306 1
        self.settings[SettingsParameters.StaticPath] = os.path.abspath(
307
            self.settings[SettingsParameters.StaticPath]
308
        )
309 1
        logger.debug(
310
            f"Static pages folder set to "
311
            f'"{self.settings[SettingsParameters.StaticPath]}"'
312
        )
313
314
        # Set subdirectory from config if applicable
315 1
        if state_config.has_option("Service Info", "Subdirectory"):
316 1
            self.subdirectory = "/" + state_config.get("Service Info", "Subdirectory")
317
318
        # If passwords file specified load credentials
319 1
        if ConfigParameters.TABPY_PWD_FILE in self.settings:
320 1
            if not self._parse_pwd_file():
321 1
                msg = (
322
                    "Failed to read passwords file "
323
                    f"{self.settings[ConfigParameters.TABPY_PWD_FILE]}"
324
                )
325 1
                logger.critical(msg)
326 1
                raise RuntimeError(msg)
327
        else:
328 1
            logger.info(
329
                "Password file is not specified: " "Authentication is not enabled"
330
            )
331
332 1
        features = self._get_features()
333 1
        self.settings[SettingsParameters.ApiVersions] = {"v1": {"features": features}}
334
335 1
        self.settings[SettingsParameters.LogRequestContext] = (
336
            self.settings[SettingsParameters.LogRequestContext].lower() != "false"
337
        )
338 1
        call_context_state = (
339
            "enabled"
340
            if self.settings[SettingsParameters.LogRequestContext]
341
            else "disabled"
342
        )
343 1
        logger.info(f"Call context logging is {call_context_state}")
344
345 1
    def _validate_transfer_protocol_settings(self):
346 1
        if SettingsParameters.TransferProtocol not in self.settings:
347
            msg = "Missing transfer protocol information."
348
            logger.critical(msg)
349
            raise RuntimeError(msg)
350
351 1
        protocol = self.settings[SettingsParameters.TransferProtocol]
352
353 1
        if protocol == "http":
354 1
            return
355
356 1
        if protocol != "https":
357 1
            msg = f"Unsupported transfer protocol: {protocol}"
358 1
            logger.critical(msg)
359 1
            raise RuntimeError(msg)
360
361 1
        self._validate_cert_key_state(
362
            "The parameter(s) {} must be set.",
363
            SettingsParameters.CertificateFile in self.settings,
364
            SettingsParameters.KeyFile in self.settings,
365
        )
366 1
        cert = self.settings[SettingsParameters.CertificateFile]
367
368 1
        self._validate_cert_key_state(
369
            "The parameter(s) {} must point to " "an existing file.",
370
            os.path.isfile(cert),
371
            os.path.isfile(self.settings[SettingsParameters.KeyFile]),
372
        )
373 1
        tabpy.tabpy_server.app.util.validate_cert(cert)
374
375 1
    @staticmethod
376
    def _validate_cert_key_state(msg, cert_valid, key_valid):
377 1
        cert_and_key_param = (
378
            f"{ConfigParameters.TABPY_CERTIFICATE_FILE} and "
379
            f"{ConfigParameters.TABPY_KEY_FILE}"
380
        )
381 1
        https_error = "Error using HTTPS: "
382 1
        err = None
383 1
        if not cert_valid and not key_valid:
384 1
            err = https_error + msg.format(cert_and_key_param)
385 1
        elif not cert_valid:
386 1
            err = https_error + msg.format(ConfigParameters.TABPY_CERTIFICATE_FILE)
387 1
        elif not key_valid:
388 1
            err = https_error + msg.format(ConfigParameters.TABPY_KEY_FILE)
389
390 1
        if err is not None:
391 1
            logger.critical(err)
392 1
            raise RuntimeError(err)
393
394 1
    def _parse_pwd_file(self):
395 1
        succeeded, self.credentials = parse_pwd_file(
396
            self.settings[ConfigParameters.TABPY_PWD_FILE]
397
        )
398
399 1
        if succeeded and len(self.credentials) == 0:
400 1
            logger.error("No credentials found")
401 1
            succeeded = False
402
403 1
        return succeeded
404
405 1
    def _get_features(self):
406 1
        features = {}
407
408
        # Check for auth
409 1
        if ConfigParameters.TABPY_PWD_FILE in self.settings:
410 1
            features["authentication"] = {
411
                "required": True,
412
                "methods": {"basic-auth": {}},
413
            }
414
415 1
        return features
416
417 1
    def _build_tabpy_state(self):
418 1
        pkg_path = os.path.dirname(tabpy.__file__)
419 1
        state_file_dir = self.settings[SettingsParameters.StateFilePath]
420 1
        state_file_path = os.path.join(state_file_dir, "state.ini")
421 1
        if not os.path.isfile(state_file_path):
422
            state_file_template_path = os.path.join(
423
                pkg_path, "tabpy_server", "state.ini.template"
424
            )
425
            logger.debug(
426
                f"File {state_file_path} not found, creating from "
427
                f"template {state_file_template_path}..."
428
            )
429
            shutil.copy(state_file_template_path, state_file_path)
430
431 1
        logger.info(f"Loading state from state file {state_file_path}")
432 1
        tabpy_state = _get_state_from_file(state_file_dir)
433
        return tabpy_state, TabPyState(config=tabpy_state, settings=self.settings)
434