Skip to content

Commit

Permalink
Merge 63ae9b9 into 4ad394f
Browse files Browse the repository at this point in the history
  • Loading branch information
stavxyz committed Sep 9, 2015
2 parents 4ad394f + 63ae9b9 commit d47202e
Show file tree
Hide file tree
Showing 9 changed files with 440 additions and 17 deletions.
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Common Python libraries for:
- [Logging](#logging)
- [Secrets](#secrets)
- [Python Utilites](#python)
- [Servers](#server)
- [WSGI Middleware](#middleware)
- [REST API Tooling](#rest)
- [Date/Time (chronos)](#chronos)
Expand Down Expand Up @@ -38,6 +39,13 @@ Code we wished was built in to python (or was simpler to use):
- dictionary and list merging
- dictionary get/set/in by path


## <a name="server"></a>Running a web service

- Perform standard webserver configuration (address, port, server adapter, etc.) using the [config](#config) module
- Run the bottle-based webservice using this configuration


## <a name="middleware"></a>WSGI middleware

Includes sample middleware for use with WSGI apps including bottle.
Expand Down
1 change: 1 addition & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ disable=W,R,I,fixme,too-many-arguments,wildcard-import,no-absolute-import,bad-bu
[BASIC]
# Don't require docstrings on tests or Python Special Methods
no-docstring-rgx=(__.*__|[tT]est.*|setUp|tearDown)
const-rgx=([A-Z_][A-Za-z0-9_]{2,30}$)
6 changes: 6 additions & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,16 @@
'Programming Language :: Python :: 3.4',
]

ENTRY_POINTS = {
'console_scripts': ['simpl=simpl.cli:main'],
}


package_attributes = {
'name': about['__title__'],
'description': about['__summary__'],
'keywords': ' '.join(about['__keywords__']),
'entry_points': ENTRY_POINTS,
'version': about['__version__'],
'tests_require': TESTS_REQUIRE,
'test_suite': 'tests',
Expand Down
62 changes: 62 additions & 0 deletions simpl/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
# Copyright 2013-2015 Rackspace US, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Simpl's base module for its command line interface."""

import functools
import logging

from simpl import server
from simpl.utils import cli as cli_utils


PARSER = cli_utils.HelpfulParser(
prog='simpl',
description='Build somthing awesome.',
)
SUBPARSER = PARSER.add_subparsers(
title='commands',
description='Available commands',
)


def default_parser():
"""Return global argumentparser."""
return PARSER


def default_subparser():
"""Return global argumentparser's subparser."""
return SUBPARSER


def main(argv=None):
"""Entry point for the `simpl` command."""
#
# `simpl server`
#
logging.basicConfig(level=logging.INFO)
server_func = functools.partial(server.main, argv=argv)
server_parser = server.attach_parser(default_subparser())
server_parser.set_defaults(_func=server_func)

# the following code shouldn't need to change when
# we add a new subcommand.
args = default_parser().parse_args(argv)
args._func()


if __name__ == '__main__':

main()
34 changes: 17 additions & 17 deletions simpl/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -347,7 +347,7 @@ class Config(collections.MutableMapping):
"""Parses configuration sources."""

def __init__(self, options=None, ini_paths=None, argv=None,
**parser_kwargs):
argparser_class=argparse.ArgumentParser, **parser_kwargs):
"""Initialize with list of options.
:param ini_paths: optional paths to ini files to look up values from
Expand All @@ -362,7 +362,8 @@ def __init__(self, options=None, ini_paths=None, argv=None,
for option in self._options}
self._argv = argv
self._metaconfigure(argv=self._argv)
self._parser = argparse.ArgumentParser(**parser_kwargs)
self._parser_class = argparser_class
self._parser = self._parser_class(**parser_kwargs)
self._prog = None
self.ini_config = None
self.pass_thru_args = []
Expand Down Expand Up @@ -446,7 +447,7 @@ def __getattr__(self, attr):
raise AttributeError("'config' object has no attribute '%s'"
% attr)

def build_parser(self, options, permissive=False, **override_kwargs):
def build_parser(self, options=None, permissive=False, **override_kwargs):
"""Construct an argparser from supplied options.
:keyword override_kwargs: keyword arguments to override when calling
Expand All @@ -460,10 +461,16 @@ def build_parser(self, options, permissive=False, **override_kwargs):
kwargs.update(override_kwargs)
if 'fromfile_prefix_chars' not in kwargs:
kwargs['fromfile_prefix_chars'] = '@'
parser = argparse.ArgumentParser(**kwargs)
if options:
for option in options:
option.add_argument(parser, permissive=permissive)
parser = self._parser_class(**kwargs)
if options is None:
options = []
for _opt in self._options:
_kw = _opt.kwargs.copy()
if _kw.get('default') is None:
_kw['default'] = argparse.SUPPRESS
options.append(Option(*_opt.args, **_kw))
for option in options:
option.add_argument(parser, permissive=permissive)
return parser

def parse_cli(self, argv=None, permissive=False):
Expand All @@ -474,20 +481,13 @@ def parse_cli(self, argv=None, permissive=False):
"""
if argv is None:
argv = self._argv or sys.argv
options = []
for option in self._options:
kwargs = option.kwargs.copy()
if kwargs.get('default') is None:
kwargs['default'] = argparse.SUPPRESS
temp = Option(*option.args, **kwargs)
options.append(temp)
parser = self.build_parser(options, permissive=permissive)
parser = self.build_parser(permissive=permissive)
parsed, extras = parser.parse_known_args(argv[1:])
if extras:
valid, pass_thru = self.parse_passthru_args(argv[1:])
parsed, extras = parser.parse_known_args(valid)
if extras and not permissive:
self.build_parser(options, permissive=permissive)
self.build_parser(permissive=permissive)
parser.parse_args(argv[1:])
self.pass_thru_args = pass_thru + extras
else:
Expand Down Expand Up @@ -524,7 +524,7 @@ def get_defaults(self):
del opt.kwargs['required']
except KeyError:
pass
parser = self.build_parser(options, permissive=True)
parser = self.build_parser(options=options, permissive=True)
parsed, _ = parser.parse_known_args([])
return vars(parsed)

Expand Down
154 changes: 154 additions & 0 deletions simpl/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,111 @@

"""Bottle Server Module."""

import copy
import logging
import os
import sys
import textwrap

import bottle

from simpl import config
from simpl.utils import cli as cli_utils

LOG = logging.getLogger(__name__)

_fill = lambda text: textwrap.fill(text, 50)

OPTIONS = [
config.Option(
'--app', '-a',
help=("WSGI application to load by name.\n"
"Ex: package.module gets the module\n"
" package.module:name gets the variable 'name'\n"
" package.module.func() calls func() and gets the result"),
group='Server Options',
),
config.Option(
'--host',
help='Server address to bind to.',
default='127.0.0.1',
group='Server Options',
),
config.Option(
'--port', '-p',
help='Server port to bind to.',
type=int,
default=8080,
group='Server Options',
),
config.Option(
'--server', '-s',
help=('Server adapter to use. To see more, run:\n'
'`python -c "import bottle;print('
'bottle.server_names.keys())"`\n'),
default='xtornado',
group='Server Options',
),
config.Option(
'--debug-server',
help=_fill(
'Run bottle server with debug=True which is useful for '
'development or troubleshooting. Warning: This may expose raw '
'tracebacks and unmodified error messages in responses! Note: '
'this is not an option to configure DEBUG level logging.'),
default=False,
action='store_true',
group='Server Options',
),
config.Option(
'--quiet-server',
help=_fill(
'Suppress bottle\'s output to stdout and stderr, e.g. '
'"Bottle v0.12.8 server starting up..." and others.'),
default=False,
action='store_true',
group='Server Options',
),
config.Option(
'--no-reloader',
default=True,
dest='reloader',
action='store_false',
help=_fill(
'Disable bottle auto-reloading server, which automatically '
'restarts the server when file changes are detected. Note: '
'some server adapters, such as eventlet, do not support '
'auto-reloading.'),
group='Server Options',
),
config.Option(
'--interval', '-i',
help='Auto-reloader interval in seconds',
type=int,
default=1,
group='Server Options',
),
config.Option(
'--adapter-options', '-o',
help=(
"Key-value pairs separated by '=' to be passed to \n"
"the underlying server adapter, e.g. XEventletServer, \n"
"and are mapped to the adapter's self.options \n"
"instance attribute. Example usage:\n"
" simpl server -s xeventlet -o keyfile=~/mykey ciphers=GOST94\n"),
nargs='*',
type=cli_utils.kwarg,
group='Server Options',
),
]

CONFIG = config.Config(
prog='simpl_server',
options=OPTIONS,
argparser_class=cli_utils.HelpfulParser,
formatter_class=cli_utils.SimplHelpFormatter
)


class EventletLogFilter(object): # pylint: disable=R0903

Expand Down Expand Up @@ -169,3 +267,59 @@ def run(self, handler):
tornado.ioloop.IOLoop.instance().start()

bottle.server_names['xtornado'] = XTornadoServer


def attach_parser(subparser):
"""Given a subparser, build and return the server parser."""
return subparser.add_parser(
'server',
help='Run a bottle based server',
parents=[
CONFIG.build_parser(
add_help=False,
# might need conflict_handler
),
],
)


def run(conf):
"""Simpl server command line interface."""
if isinstance(conf.adapter_options, list):
options = {key: val for _dict in conf.adapter_options
for key, val in _dict.items()}
elif conf.adapter_options is None:
options = {}
else:
options = copy.copy(conf.adapter_options)

# waiting for https://github.com/bottlepy/bottle/pull/783
if conf.app and (os.getcwd() not in sys.path):
sys.path.append(os.getcwd())

try:
if conf.reloader and not os.getenv('BOTTLE_CHILD'):
LOG.info("Running bottle server with reloader.")
return bottle.run(
app=conf.app,
server=conf.server,
host=conf.host,
port=conf.port,
interval=conf.interval,
reloader=conf.reloader,
quiet=conf.quiet_server,
debug=conf.debug_server,
**options
)
except KeyboardInterrupt:
sys.exit("\nKilled simpl server.")


def main(argv=None):
"""Entry point for server, runs based on parsed CONFIG."""
CONFIG.parse(argv=argv)
return run(CONFIG)


if __name__ == '__main__':
main()
Loading

0 comments on commit d47202e

Please sign in to comment.