Passed
Pull Request — master (#543)
by
unknown
13:14
created

tabpy.tabpy_server.app.app   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 446
Duplicated Lines 0 %

Test Coverage

Coverage 80.61%

Importance

Changes 0
Metric Value
wmc 54
eloc 302
dl 0
loc 446
ccs 158
cts 196
cp 0.8061
rs 6.4799
c 0
b 0
f 0

10 Methods

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