Passed
Push — master ( 305552...00a4d3 )
by Oleksandr
02:44
created

tabpy.tabpy_server.app.app   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 401
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 43
eloc 290
dl 0
loc 401
rs 8.96
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A TabPyApp._parse_pwd_file() 0 9 3
B TabPyApp._validate_cert_key_state() 0 18 6
A TabPyApp.__init__() 0 18 5
A TabPyApp._validate_transfer_protocol_settings() 0 28 4
F TabPyApp._parse_config() 0 173 16
A TabPyApp._get_features() 0 10 2
B TabPyApp._create_tornado_web_app() 0 52 3
A TabPyApp.run() 0 34 3
A TabPyApp._parse_cli_arguments() 0 8 1

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