Passed
Push — master ( 2d3a5f...3cd106 )
by Oleksandr
13:57 queued 06:14
created

tabpy.tabpy_server.app.app   B

Complexity

Total Complexity 50

Size/Duplication

Total Lines 431
Duplicated Lines 0 %

Test Coverage

Coverage 82.16%

Importance

Changes 0
Metric Value
wmc 50
eloc 284
dl 0
loc 431
ccs 152
cts 185
cp 0.8216
rs 8.4
c 0
b 0
f 0

10 Methods

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