Пример #1
0
 def test_schema_correct(self):
     assert SettingsTwoFour.schema == {
         'bar':
         fields.Integer(),
         'baz':
         fields.Dictionary(
             {
                 'inner_foo':
                 fields.UnicodeString(),
                 'inner_bar':
                 fields.Boolean(),
                 'inner_baz':
                 fields.List(fields.Integer()),
                 'inner_qux':
                 fields.Dictionary(
                     {
                         'most_inner_foo': fields.Boolean(),
                         'most_inner_bar': fields.UnicodeString(),
                     }, ),
             }, ),
         'qux':
         fields.Decimal(),
         'new':
         fields.ByteString(),
         'old':
         fields.UnicodeString(),
     }
Пример #2
0
class SettingsTwo(Settings):
    schema = {
        'bar':
        fields.Integer(),
        'baz':
        fields.Dictionary(
            {
                'inner_foo':
                fields.UnicodeString(),
                'inner_bar':
                fields.Boolean(),
                'inner_baz':
                fields.List(fields.Integer()),
                'inner_qux':
                fields.Dictionary(
                    {
                        'most_inner_foo': fields.Boolean(),
                        'most_inner_bar': fields.UnicodeString(),
                    }, ),
            }, ),
    }  # type: SettingsSchema

    defaults = {
        'bar': 1,
        'baz': {
            'inner_foo': 'Default inner',
            'inner_qux': {
                'most_inner_bar': 'Default most inner'
            }
        },
    }  # type: SettingsData
Пример #3
0
 def test_schema_correct(self):
     assert SettingsOneTwoThree.schema == {
         'foo':
         fields.UnicodeString(),
         'bar':
         fields.Boolean(),
         'baz':
         fields.Dictionary(
             {
                 'inner_foo':
                 fields.UnicodeString(),
                 'inner_bar':
                 fields.Boolean(),
                 'inner_baz':
                 fields.List(fields.Integer()),
                 'inner_qux':
                 fields.Dictionary(
                     {
                         'most_inner_foo': fields.Boolean(),
                         'most_inner_bar': fields.UnicodeString(),
                     }, ),
             }, ),
         'qux':
         fields.Float(),
     }
Пример #4
0
class SettingsToTest(settings.Settings):
    schema: settings.SettingsSchema = {
        'one': fields.Dictionary({
            'a': fields.ClassConfigurationSchema(base_class=ClassUsingAttrs27HintsToTest, description='Nifty schema.'),
            'b': fields.PythonPath(value_schema=fields.UnicodeString(), description='Must be a path, yo.'),
            'c': fields.TypeReference(base_classes=ClassHoldingSigsToTest, description='Refer to that thing!'),
        }),
        'two': fields.SchemalessDictionary(key_type=fields.UnicodeString(), value_type=fields.Boolean()),
        'three': fields.List(fields.Integer()),
        'four': fields.Nullable(fields.Set(fields.ByteString())),
        'five': fields.Any(fields.Integer(), fields.Float()),
        'six': fields.ObjectInstance(valid_type=ClassUsingAttrs27HintsToTest, description='Y u no instance?'),
        'seven': fields.Polymorph(
            'thing',
            {
                'thing1': fields.Dictionary({'z': fields.Boolean()}, allow_extra_keys=True),
                'thing2': fields.Dictionary({'y': fields.Boolean()}, allow_extra_keys=True, optional_keys=('y', )),
            },
        ),
    }

    defaults: settings.SettingsData = {
        'one': {
            'b': 'foo.bar:Class',
        },
        'three': [1, 5, 7],
    }
Пример #5
0
class TypesEchoAction(Action):
    request_schema = fields.Dictionary(
        {
            'an_int': fields.Integer(),
            'a_float': fields.Float(),
            'a_bool': fields.Boolean(),
            'a_bytes': fields.ByteString(),
            'a_string': fields.UnicodeString(),
            'a_datetime': fields.DateTime(),
            'a_date': fields.Date(),
            'a_time': fields.Time(),
            'a_list': fields.List(fields.Anything(), max_length=0),
            'a_dict': fields.Nullable(fields.Dictionary({})),
        },
        optional_keys=(
            'an_int', 'a_float', 'a_bool', 'a_bytes', 'a_string', 'a_datetime', 'a_date', 'a_time', 'a_list', 'a_dict',
        ),
    )

    response_schema = fields.Dictionary(
        {'r_{}'.format(k): v for k, v in six.iteritems(request_schema.contents)},
        optional_keys=('r_{}'.format(k) for k in request_schema.optional_keys),
    )

    def run(self, request):
        return {'r_{}'.format(k): v for k, v in six.iteritems(request.body)}
Пример #6
0
 def test_schema_correct(self):
     assert SettingsOneTwoThreeWithOverrides.schema == {
         'foo': fields.UnicodeString(),
         'bar': fields.Boolean(),
         'baz': fields.ByteString(),
         'qux': fields.Float(),
     }
Пример #7
0
 def test_schema_correct(self):
     assert SettingsOneThree.schema == {
         'foo': fields.UnicodeString(),
         'bar': fields.Boolean(),
         'baz': fields.List(fields.Float()),
         'qux': fields.Float(),
     }
Пример #8
0
 def test_schema_correct(self):
     assert SettingsOneFour.schema == {
         'foo': fields.UnicodeString(),
         'bar': fields.Boolean(),
         'baz': fields.List(fields.Float()),
         'qux': fields.Decimal(),
         'new': fields.ByteString(),
         'old': fields.UnicodeString(),
     }
Пример #9
0
        class NotASettings(object):
            schema = {
                'foo': fields.UnicodeString(),
                'bar': fields.Boolean(),
            }  # type: SettingsSchema

            defaults = {
                'bar': False,
            }  # type: SettingsData
Пример #10
0
 def test_schema_correct(self):
     assert SettingsOneTwoThreeWithOverridesExtended.schema == {
         'foo': fields.UnicodeString(),
         'bar': fields.Boolean(),
         'baz': fields.ByteString(),
         'qux': fields.Decimal(),
         'new': fields.ByteString(),
         'old': fields.UnicodeString(),
     }
Пример #11
0
class SettingsOne(Settings):
    schema = {
        'foo': fields.UnicodeString(),
        'bar': fields.Boolean(),
    }  # type: SettingsSchema

    defaults = {
        'bar': False,
    }  # type: SettingsData
Пример #12
0
class FakeActionTwo(object):
    """Test action documentation"""

    request_schema = fields.UnicodeString(description='Be weird.')

    response_schema = fields.Dictionary({
        'okay':
        fields.Boolean(description='Whether it is okay'),
        'reason':
        fields.Nullable(
            fields.UnicodeString(description='Why it is not okay')),
    })
Пример #13
0
class TestAction(Action):
    __test__ = False  # So that PyTest doesn't try to collect this and spit out a warning

    request_schema = fields.Dictionary({
        'string_field': fields.UnicodeString(),
    })  # type: Optional[fields.Dictionary]

    response_schema = fields.Dictionary({
        'boolean_field': fields.Boolean(),
    })  # type: Optional[fields.Dictionary]

    _return = None  # type: Optional[Dict[six.text_type, Any]]

    def run(self, request):
        return self._return
Пример #14
0
class TestAction(Action):
    __test__ = False  # So that PyTest doesn't try to collect this and spit out a warning

    request_schema = fields.Dictionary({
        'string_field': fields.UnicodeString(),
    })

    response_schema = fields.Dictionary({
        'boolean_field': fields.Boolean(),
    })

    _return = None

    def run(self, request):
        return self._return
Пример #15
0
    'Configuration',
    'CONFIGURATION_SCHEMA',
    'create_configuration',
)

CONFIGURATION_SCHEMA = fields.Polymorph(
    switch_field='version',
    contents_map={
        2:
        fields.Dictionary(
            {
                'version':
                fields.Constant(2),
                'enable_meta_metrics':
                fields.Boolean(
                    description=
                    'If true, meta-metrics will be recorded documenting the performance of '
                    'PyMetrics itself.', ),
                'error_logger_name':
                fields.UnicodeString(
                    description=
                    'By default, errors encountered when publishing metrics are suppressed and lost. If '
                    'this value is truthy, a Logger is created with this name and used to log publication '
                    'errors.', ),
                'publishers':
                fields.Sequence(
                    fields.ClassConfigurationSchema(
                        base_class=MetricsPublisher,
                        description=
                        'Import path and arguments for a publisher.',
                    ),
                    min_length=1,
Пример #16
0
            },
        ),
    }

    defaults: settings.SettingsData = {
        'one': {
            'b': 'foo.bar:Class',
        },
        'three': [1, 5, 7],
    }


@fields.ClassConfigurationSchema.provider(fields.Dictionary(
    collections.OrderedDict((
        ('one', fields.UnicodeString()),
        ('two', fields.Boolean()),
        ('three', fields.Decimal()),
    )),
    description='This is the neatest documentation for a class',
))
class ClassConfigurationToTest:
    pass


@pytest.mark.parametrize(
    ('obj', 'annotations'),
    (
        (ClassHoldingSigsToTest.sig1, {'one': AnyStr, 'two': Optional[bool], 'return': None}),
        (ClassHoldingSigsToTest.sig1_35, {'one': AnyStr, 'two': Optional[bool], 'return': None}),
        (ClassHoldingSigsToTest.sig2, {'one': bool, 'two': Optional[AnyStr], 'args': int, 'return': List[int]}),
        (ClassHoldingSigsToTest.sig2_35, {'one': bool, 'two': Optional[AnyStr], 'args': int, 'return': List[int]}),
Пример #17
0
class ServerSettings(SOASettings):
    """
    Settings specific to servers
    """

    schema = {
        'transport':
        BasicClassSchema(BaseServerTransport),
        'middleware':
        fields.List(
            BasicClassSchema(ServerMiddleware),
            description=
            'The list of all `ServerMiddleware` objects that should be applied to requests processed by '
            'this server',
        ),
        'client_routing':
        fields.SchemalessDictionary(
            key_type=fields.UnicodeString(),
            value_type=fields.SchemalessDictionary(),
            description=
            'Client settings for sending requests to other services; keys should be service names, and '
            'values should be the corresponding configuration dicts, which will be validated using the '
            'PolymorphicClientSettings schema',
        ),
        'logging':
        fields.Dictionary(
            {
                'version':
                fields.Integer(gte=1, lte=1),
                'formatters':
                fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary(
                        {
                            'format': fields.UnicodeString(),
                            'datefmt': fields.UnicodeString(),
                        },
                        optional_keys=('datefmt', ),
                    ),
                ),
                'filters':
                fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary(
                        {
                            '()':
                            fields.Anything(
                                description='The optional filter class'),
                            'name':
                            fields.UnicodeString(
                                description='The optional filter name'),
                        },
                        optional_keys=('()', 'name'),
                    ),
                ),
                'handlers':
                fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary(
                        {
                            'class': fields.UnicodeString(),
                            'level': fields.UnicodeString(),
                            'formatter': fields.UnicodeString(),
                            'filters': fields.List(fields.UnicodeString()),
                        },
                        optional_keys=('level', 'formatter', 'filters'),
                        allow_extra_keys=True,
                    ),
                ),
                'loggers':
                fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=_logger_schema,
                ),
                'root':
                _logger_schema,
                'incremental':
                fields.Boolean(),
                'disable_existing_loggers':
                fields.Boolean(),
            },
            optional_keys=(
                'version',
                'formatters',
                'filters',
                'handlers',
                'root',
                'loggers',
                'incremental',
            ),
            description=
            'Settings for service logging, which should follow the standard Python logging configuration',
        ),
        'harakiri':
        fields.Dictionary(
            {
                'timeout':
                fields.Integer(
                    gte=0,
                    description=
                    'Seconds of inactivity before harakiri is triggered; 0 to disable, defaults to 300',
                ),
                'shutdown_grace':
                fields.Integer(
                    gt=0,
                    description=
                    'Seconds to forcefully shutdown after harakiri is triggered if shutdown does not occur',
                ),
            },
            description=
            'Instructions for automatically terminating a server process when request processing takes '
            'longer than expected.',
        ),
        'request_log_success_level':
        log_level_schema(
            description=
            'The logging level at which full request and response contents will be logged for successful '
            'requests', ),
        'request_log_error_level':
        log_level_schema(
            description=
            'The logging level at which full request and response contents will be logged for requests '
            'whose responses contain errors (setting this to a more severe level than '
            '`request_log_success_level` will allow you to easily filter for unsuccessful requests)',
        ),
        'heartbeat_file':
        fields.Nullable(
            fields.UnicodeString(
                description=
                'If specified, the server will create a heartbeat file at the specified path on startup, '
                'update the timestamp in that file after the processing of every request or every time '
                'idle operations are processed, and delete the file when the server shuts down. The file name '
                'can optionally contain the specifier {{pid}}, which will be replaced with the server process '
                'PID.', )),
        'extra_fields_to_redact':
        fields.Set(
            fields.UnicodeString(),
            description=
            'Use this field to supplement the set of fields that are automatically redacted/censored in '
            'request and response fields with additional fields that your service needs redacted.',
        ),
    }

    defaults = {
        'client_routing': {},
        'logging': {
            'version': 1,
            'formatters': {
                'console': {
                    'format':
                    '%(asctime)s %(levelname)7s %(correlation_id)s %(request_id)s: %(message)s'
                },
                'syslog': {
                    'format':
                    ('%(service_name)s_service: %(name)s %(levelname)s %(module)s %(process)d '
                     'correlation_id %(correlation_id)s request_id %(request_id)s %(message)s'
                     ),
                },
            },
            'filters': {
                'pysoa_logging_context_filter': {
                    '()': 'pysoa.common.logging.PySOALogContextFilter',
                },
            },
            'handlers': {
                'console': {
                    'level': 'INFO',
                    'class': 'logging.StreamHandler',
                    'formatter': 'console',
                    'filters': ['pysoa_logging_context_filter'],
                },
                'syslog': {
                    'level': 'INFO',
                    'class': 'logging.handlers.SysLogHandler',
                    'facility': SysLogHandler.LOG_LOCAL7,
                    'address': ('localhost', 514),
                    'formatter': 'syslog',
                    'filters': ['pysoa_logging_context_filter'],
                },
            },
            'loggers': {},
            'root': {
                'handlers': ['console'],
                'level': 'INFO',
            },
            'disable_existing_loggers': False,
        },
        'harakiri': {
            'timeout': 300,
            'shutdown_grace': 30,
        },
        'request_log_success_level': 'INFO',
        'request_log_error_level': 'INFO',
        'heartbeat_file': None,
        'extra_fields_to_redact': set(),
    }
Пример #18
0
from conformity import fields

from pysoa.common.schemas import (
    BasicClassSchema,
    PolymorphClassSchema,
)
from pysoa.common.settings import SOASettings
from pysoa.common.transport.base import ServerTransport as BaseServerTransport
from pysoa.common.transport.local import LocalServerTransportSchema
from pysoa.common.transport.redis_gateway.settings import RedisTransportSchema
from pysoa.server.middleware import ServerMiddleware

_logger_schema = fields.Dictionary(
    {
        'level': fields.UnicodeString(),
        'propagate': fields.Boolean(),
        'filters': fields.List(fields.UnicodeString()),
        'handlers': fields.List(fields.UnicodeString()),
    },
    optional_keys=('level', 'propagate', 'filters', 'handlers'),
)

log_level_schema = functools.partial(fields.Constant, 'DEBUG', 'INFO',
                                     'WARNING', 'ERROR', 'CRITICAL')


class ServerSettings(SOASettings):
    """
    Settings specific to servers
    """
Пример #19
0
class BaseStatusAction(Action):
    """
    Standard base action for status checks.

    Returns heath check and version information.

    If you want to use default StatusAction use StatusActionFactory(version)
    passing in the version of your service.

    If you want to make a custom StatusAction, subclass this class,
    make it get self._version from your service and add additional health check methods.
    Health check methods must start with the word check_.

    Health check methods must take no arguments, and return a list of tuples in the format:
    (is_error, code, description).

    is_error: True if this is an error, False if it is a warning.
    code: Invariant string for this error, like "MYSQL_FAILURE"
    description: Human-readable description of the problem, like "Could not connect to host on port 1234"

    Health check methods can also write to the self.diagnostics dictionary to add additional
    data which will be sent back with the response if they like. They are responsible for their
    own key management in this situation.
    """

    def __init__(self, *args, **kwargs):
        if self.__class__ is BaseStatusAction:
            raise RuntimeError('You cannot use BaseStatusAction directly; it must be subclassed')
        super(BaseStatusAction, self).__init__(*args, **kwargs)

        self.diagnostics = {}

    @property
    def _version(self):
        raise NotImplementedError('version must be defined using StatusActionFactory')

    @property
    def _build(self):
        return None

    request_schema = fields.Dictionary(
        {
            'verbose': fields.Boolean(
                description='If specified and False, this instructs the status action to return only the baseline '
                            'status information (Python, service, PySOA, and other library versions) and omit any of '
                            'the health check operations (no `healthcheck` attribute will be included in the '
                            'response). This provides a useful way to obtain the service version very quickly without '
                            'executing the often time-consuming code necessary for the full health check. It defaults '
                            'to True, which means "return everything."',
            ),
        },
        optional_keys=('verbose', ),
    )

    response_schema = fields.Dictionary(
        {
            'build': fields.UnicodeString(),
            'conformity': fields.UnicodeString(),
            'healthcheck': fields.Dictionary(
                {
                    'warnings': fields.List(fields.Tuple(fields.UnicodeString(), fields.UnicodeString())),
                    'errors': fields.List(fields.Tuple(fields.UnicodeString(), fields.UnicodeString())),
                    'diagnostics': fields.SchemalessDictionary(key_type=fields.UnicodeString()),
                },
                optional_keys=('warnings', 'errors', 'diagnostics'),
            ),
            'pysoa': fields.UnicodeString(),
            'python': fields.UnicodeString(),
            'version': fields.UnicodeString(),
        },
        optional_keys=('build', 'healthcheck', ),
    )

    def run(self, request):
        """
        Scans the class for check_ methods and runs them.
        """
        status = {
            'conformity': six.text_type(conformity.__version__),
            'pysoa': six.text_type(pysoa.__version__),
            'python': six.text_type(platform.python_version()),
            'version': self._version,
        }

        if self._build:
            status['build'] = self._build

        if request.body.get('verbose', True) is True:
            errors = []
            warnings = []
            self.diagnostics = {}

            # Find all things called "check_<something>" on this class.
            # We can't just scan __dict__ because of class inheritance.
            check_methods = [getattr(self, x) for x in dir(self) if x.startswith('check_')]
            for check_method in check_methods:
                # Call the check, and see if it returned anything
                problems = check_method()
                if problems:
                    for is_error, code, description in problems:
                        # Parcel out the values into the right return list
                        if is_error:
                            errors.append((code, description))
                        else:
                            warnings.append((code, description))

            status['healthcheck'] = {
                'errors': errors,
                'warnings': warnings,
                'diagnostics': self.diagnostics,
            }

        return status
Пример #20
0
class Sqlite3Connection(sqlite3.Connection):
    """
    An extension to the base connection. The base class is a pure C class on whose instances you can't call setattr.
    This extension enables the use of setattr on connection objects.
    """


@fields.ClassConfigurationSchema.provider(
    fields.Dictionary(
        {
            'database_name':
            fields.UnicodeString(
                description='The name of the Sqlite database to use'),
            'use_uri':
            fields.Boolean(
                description=
                'Whether the database name should be treated as a URI (Python 3+ only)',
            ),
        },
        optional_keys=('database_name', 'use_uri'),
    ))
class SqlitePublisher(SqlPublisher):
    """
    A publisher that emits metrics to a Sqlite database file or in-memory database. Especially useful for use in tests
    where you need to actually evaluate your metrics.
    """

    database_type = 'Sqlite'
    exception_type = sqlite3.Error

    MEMORY_DATABASE_NAME = ':memory:'
Пример #21
0
 def test_schema_correct(self):
     assert SettingsOne.schema == {
         'foo': fields.UnicodeString(),
         'bar': fields.Boolean(),
     }
Пример #22
0
        ),
        'handlers':
        fields.List(
            fields.UnicodeString(),
            description=
            'A list of references to keys from `handlers` for assigning those handlers to this logger.',
        ),
    },
    optional_keys=('level', 'filters', 'handlers'),
)

PYTHON_LOGGER_SCHEMA = PYTHON_ROOT_LOGGER_SCHEMA.extend(
    contents={
        'propagate':
        fields.Boolean(
            description=
            'Whether logging events handled by this logger should propagate to other loggers and/or the '
            'root logger. Defaults to `True`.'),
    },
    optional_keys=('propagate', ),
)

PYTHON_LOGGING_CONFIG_SCHEMA = fields.Dictionary(
    collections.OrderedDict((
        ('version', fields.Integer(gte=1, lte=1)),
        ('formatters',
         fields.SchemalessDictionary(
             key_type=fields.UnicodeString(),
             value_type=fields.Dictionary(
                 {
                     'format':
                     fields.UnicodeString(
Пример #23
0
class ServerSettings(SOASettings):
    """
    Settings specific to servers
    """

    schema = {
        'transport': BasicClassSchema(BaseServerTransport),
        'middleware': fields.List(BasicClassSchema(ServerMiddleware)),
        'client_routing': fields.SchemalessDictionary(
            key_type=fields.UnicodeString(),
            value_type=fields.SchemalessDictionary(),
            description='Client settings for sending requests to other services; keys should be service names, and '
                        'values should be the corresponding configuration dicts, which will be validated using the '
                        'PolymorphicClientSettings schema',
        ),
        'logging': fields.Dictionary(
            {
                'version': fields.Integer(gte=1, lte=1),
                'formatters': fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary(
                        {
                            'format': fields.UnicodeString(),
                            'datefmt': fields.UnicodeString(),
                        },
                        optional_keys=('datefmt', ),
                    ),
                ),
                'filters': fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary({'name': fields.UnicodeString()}, optional_keys=('name', )),
                ),
                'handlers': fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=fields.Dictionary(
                        {
                            'class': fields.UnicodeString(),
                            'level': fields.UnicodeString(),
                            'formatter': fields.UnicodeString(),
                            'filters': fields.List(fields.UnicodeString()),
                        },
                        optional_keys=('level', 'formatter', 'filters'),
                        allow_extra_keys=True,
                    ),
                ),
                'loggers': fields.SchemalessDictionary(
                    key_type=fields.UnicodeString(),
                    value_type=_logger_schema,
                ),
                'root': _logger_schema,
                'incremental': fields.Boolean(),
                'disable_existing_loggers': fields.Boolean(),
            },
            optional_keys=(
                'version',
                'formatters',
                'filters',
                'handlers',
                'root',
                'loggers',
                'incremental',
                'disable_existing_loggers',
            ),
            description='Settings for service logging, which should follow the standard Python logging configuration',
        ),
        'harakiri': fields.Dictionary(
            {
                'timeout': fields.Integer(
                    gte=0,
                    description='Seconds of inactivity before harakiri is triggered; 0 to disable, defaults to 300',
                ),
                'shutdown_grace': fields.Integer(
                    gt=0,
                    description='Seconds to forcefully shutdown after harakiri is triggered if shutdown does not occur',
                ),
            },
        ),
    }

    defaults = {
        'client_routing': {},
        'logging': {
            'version': 1,
            'formatters': {
                'console': {
                    'format': '%(asctime)s %(levelname)7s: %(message)s'
                },
            },
            'handlers': {
                'console': {
                    'level': 'INFO',
                    'class': 'logging.StreamHandler',
                    'formatter': 'console',
                },
            },
            'root': {
                'handlers': ['console'],
                'level': 'INFO',
            },
        },
        'harakiri': {
            'timeout': 300,
            'shutdown_grace': 30,
        },
    }
Пример #24
0
class ExpansionSettings(Settings):
    """
    Defines the schema for configuration settings used when expanding objects on responses with the Expansions tool.
    """

    schema = {
        'type_routes':
        fields.SchemalessDictionary(
            key_type=fields.UnicodeString(
                description=
                'The name of the expansion route, to be referenced from the `type_expansions` '
                'configuration', ),
            value_type=fields.Dictionary(
                {
                    'service':
                    fields.UnicodeString(
                        description=
                        'The name of the service to call to resolve this route',
                    ),
                    'action':
                    fields.UnicodeString(
                        description=
                        'The name of the action to call to resolve this route, which must accept a single '
                        'request field of type `List`, to which all the identifiers for matching candidate '
                        'expansions will be passed, and which must return a single response field of type '
                        '`Dictionary`, from which all expansion objects will be obtained',
                    ),
                    'request_field':
                    fields.UnicodeString(
                        description=
                        'The name of the `List` identifier field to place in the `ActionRequest` body when '
                        'making the request to the named service and action',
                    ),
                    'response_field':
                    fields.UnicodeString(
                        description=
                        'The name of the `Dictionary` field returned in the `ActionResponse`, from which '
                        'the expanded objects will be extracted', ),
                },
                description='The instructions for resolving this type route',
            ),
            description=
            'The definition of all recognized types that can be expanded into and information about how '
            'to resolve objects of those types through action calls',
        ),
        'type_expansions':
        fields.SchemalessDictionary(
            key_type=fields.UnicodeString(
                description=
                'The name of the type for which the herein defined expansions can be sought, which will be '
                "matched with a key from the `expansions` dict passed to one of `Client`'s `call_***` "
                'methods, and which must also match the value of a `_type` field found on response objects '
                'on which extra data will be expanded', ),
            value_type=fields.SchemalessDictionary(
                key_type=fields.UnicodeString(
                    description=
                    'The name of an expansion, which will be matched with a value from the `expansions` '
                    "dict passed to one of `Client`'s `call_***` methods corresponding to the type key in "
                    'that dict', ),
                value_type=fields.Dictionary(
                    {
                        'type':
                        fields.Nullable(
                            fields.UnicodeString(
                                description=
                                'The type of object this expansion yields, which must map back to a '
                                '`type_expansions` key in order to support nested/recursive expansions, and '
                                'may be `None` if you do not wish to support nested/recursive expansions for '
                                'this expansion', )),
                        'route':
                        fields.UnicodeString(
                            description=
                            'The route to use to resolve this expansion, which must match a key in the '
                            '`type_routes` configuration', ),
                        'source_field':
                        fields.UnicodeString(
                            description=
                            'The name of the field in the base object that contains the identifier used '
                            'for obtaining the expansion object (the identifier will be passed to the '
                            '`request_field` in the route when resolving the expansion)',
                        ),
                        'destination_field':
                        fields.UnicodeString(
                            description=
                            'The name of a not-already-existent field in the base object into which the '
                            'expansion object will be placed after it is obtained from the route',
                        ),
                        'raise_action_errors':
                        fields.Boolean(
                            description=
                            'Whether to raise action errors encountered when expanding objects these '
                            'objects (by default, action errors are suppressed, which differs from the '
                            'behavior of the `Client` to raise action errors during normal requests)',
                        ),
                    },
                    optional_keys=('raise_action_errors', ),
                    description=
                    'The definition of one specific possible expansion for this object type',
                ),
                description=
                'The definition of all possible expansions for this object type',
            ),
            description=
            'The definition of all types that may contain identifiers that can be expanded into objects '
            'using the `type_routes` configurations',
        ),
    }
Пример #25
0
class BaseStatusAction(Action):
    """
    Standard base action for status checks. Returns health check and version information.

    If you want to use the status action use `StatusActionFactory(version)`, passing in the version of your service
    and, optionally, the build of your service. If you do not specify an action with name `status` in your server,
    this will be done on your behalf.

    If you want to make a custom status action, subclass this class, make `self._version` return your service's version
    string, `self._build` optionally return your service's build string, and add any additional health check methods
    you desire. Health check methods must start with `check_`.

    Health check methods accept a single argument, the request object (an instance of `ActionRequest`), and return a
    list of tuples in the format `(is_error, code, description)` (or a false-y value if there are no problems):

    - `is_error`: `True` if this is an error, `False` if it is a warning.
    - `code`: Invariant string for this error, like "MYSQL_FAILURE"
    - `description`: Human-readable description of the problem, like "Could not connect to host on port 1234"

    Health check methods can also write to the `self.diagnostics` dictionary to add additional data which will be sent
    back with the response if they like. They are responsible for their own key management in this situation.

    This base status action comes with a disabled-by-default health check method named `_check_client_settings` (the
    leading underscore disables it), which calls `status` on all other services that this service is configured to call
    (using `verbose: False`, which guarantees no further recursive status checking) and includes those responses in
    this action's response. To enable this health check, simply reference it as a new, valid `check_` method name, like
    so:

    .. code:: python

        class MyStatusAction(BaseStatusAction):
            ...
            check_client_settings = BaseStatusAction._check_client_settings
    """
    def __init__(self, *args, **kwargs):
        """
        Constructs a new base status action. Concrete status actions can override this if they want, but must call
        `super`.

        :param settings: The server settings object
        :type settings: dict
        """
        super(BaseStatusAction, self).__init__(*args, **kwargs)

        self.diagnostics = {}

    @abc.abstractproperty
    def _version(self):
        raise NotImplementedError(
            'version must be defined using StatusActionFactory')

    @property
    def _build(self):
        return None

    description = (
        'Returns version info for the service, Python, PySOA, and Conformity. If the service has a build string, that '
        'is also returned. If the service has defined additional health check behavior and the `verbose` request '
        'attribute is not set to `False`, those additional health checks are performed and returned in the '
        '`healthcheck` response attribute. If the `verbose` request attribute is set to `False`, the additional '
        'health checks are not performed and `healthcheck` is not included in the response (importantly, the `check_` '
        'methods are not invoked).')

    request_schema = fields.Nullable(
        fields.Dictionary(
            {
                'verbose':
                fields.Boolean(
                    description=
                    'If specified and False, this instructs the status action to return only the baseline '
                    'status information (Python, service, PySOA, and other library versions) and omit any of '
                    'the health check operations (no `healthcheck` attribute will be included in the '
                    'response). This provides a useful way to obtain the service version very quickly without '
                    'executing the often time-consuming code necessary for the full health check. It defaults '
                    'to True, which means "return everything."', ),
            },
            optional_keys=('verbose', ),
        ))

    response_schema = fields.Dictionary(
        {
            'build':
            fields.UnicodeString(
                description='The version build string, if applicable.'),
            'conformity':
            fields.UnicodeString(
                description='The version of Conformity in use.'),
            'healthcheck':
            fields.Dictionary(
                {
                    'warnings':
                    fields.List(
                        fields.Tuple(
                            fields.UnicodeString(
                                description='The invariant warning code'),
                            fields.UnicodeString(
                                description='The readable warning description'
                            ),
                        ),
                        description=
                        'A list of any warnings encountered during the health checks.',
                    ),
                    'errors':
                    fields.List(
                        fields.Tuple(
                            fields.UnicodeString(
                                description='The invariant error code'),
                            fields.UnicodeString(
                                description='The readable error description'),
                        ),
                        description=
                        'A list of any errors encountered during the health checks.',
                    ),
                    'diagnostics':
                    fields.SchemalessDictionary(
                        key_type=fields.UnicodeString(),
                        description=
                        'A dictionary containing any additional diagnostic information output by the '
                        'health check operations.',
                    ),
                },
                optional_keys=('warnings', 'errors', 'diagnostics'),
                description=
                'Information about any additional health check operations performed.',
            ),
            'pysoa':
            fields.UnicodeString(description='The version of PySOA in use.'),
            'python':
            fields.UnicodeString(description='The version of Python in use.'),
            'version':
            fields.UnicodeString(
                description='The version of the responding service.'),
        },
        optional_keys=(
            'build',
            'healthcheck',
        ),
    )

    def run(self, request):
        """
        Adds version information for Conformity, PySOA, Python, and the service to the response, then scans the class
        for `check_` methods and runs them (unless `verbose` is `False`).

        :param request: The request object
        :type request: EnrichedActionRequest

        :return: The response
        """
        status = {
            'conformity': six.text_type(conformity.__version__),
            'pysoa': six.text_type(pysoa.__version__),
            'python': six.text_type(platform.python_version()),
            'version': self._version,
        }

        if self._build:
            status['build'] = self._build

        if not request.body or request.body.get('verbose', True) is True:
            errors = []
            warnings = []
            self.diagnostics = {}

            # Find all things called "check_<something>" on this class.
            # We can't just scan __dict__ because of class inheritance.
            check_methods = [
                getattr(self, x) for x in dir(self) if x.startswith('check_')
            ]
            for check_method in check_methods:
                # Call the check, and see if it returned anything
                try:
                    problems = check_method(request)
                except TypeError as e:
                    raise RuntimeError(
                        'Status action check_* methods must accept a single argument of type ActionRequest',
                        e,
                    )
                if problems:
                    for is_error, code, description in problems:
                        # Parcel out the values into the right return list
                        if is_error:
                            errors.append((code, description))
                        else:
                            warnings.append((code, description))

            status['healthcheck'] = {
                'errors': errors,
                'warnings': warnings,
                'diagnostics': self.diagnostics,
            }

        return status

    def _check_client_settings(self, request):
        """
        This method checks any client settings configured for this service to call other services, calls the `status`
        action of each configured service with `verbose: False` (which guarantees no further recursive status checking),
        adds that diagnostic information, and reports any problems. To include this check in your status action, define
        `check_client_settings = BaseStatusAction._check_client_settings` in your status action class definition.
        """
        if not request.client.settings:
            # There's no need to even add diagnostic details if no client settings are configured
            return

        self.diagnostics['services'] = {}

        service_names = list(six.iterkeys(request.client.settings))
        try:
            job_responses = request.client.call_jobs_parallel(
                [{
                    'service_name': service_name,
                    'actions': [{
                        'action': 'status',
                        'body': {
                            'verbose': False
                        }
                    }]
                } for service_name in service_names],
                timeout=2,
                catch_transport_errors=True,
                raise_action_errors=False,
                raise_job_errors=False,
            )
        except Exception as e:
            return [(True, 'CHECK_SERVICES_UNKNOWN_ERROR', six.text_type(e))]

        problems = []
        for i, service_name in enumerate(service_names):
            response = job_responses[i]
            if isinstance(response, Exception):
                problems.append((True, '{}_TRANSPORT_ERROR'.format(
                    service_name.upper()), six.text_type(response)), )
            elif response.errors:
                problems.append((True, '{}_CALL_ERROR'.format(
                    service_name.upper()), six.text_type(response.errors)), )
            elif response.actions[0].errors:
                problems.append(
                    (True, '{}_STATUS_ERROR'.format(service_name.upper()),
                     six.text_type(response.actions[0].errors)), )
            else:
                self.diagnostics['services'][service_name] = response.actions[
                    0].body

        return problems
Пример #26
0
from pymetrics.instruments import (
    Counter,
    Gauge,
    Histogram,
    Metric,
    Tag,
    Timer,
    TimerResolution,
)
from pymetrics.publishers.statsd import StatsdPublisher

__all__ = ('DogStatsdPublisher', )

_datadog_tags_value_type = fields.Nullable(
    fields.Any(fields.UnicodeString(), fields.ByteString(), fields.Integer(),
               fields.Float(), fields.Boolean()), )


@fields.ClassConfigurationSchema.provider(
    fields.Dictionary(
        {
            'host':
            fields.UnicodeString(
                description=
                'The host name or IP address on which the Dogstatsd server is listening',
            ),
            'port':
            fields.Integer(
                description=
                'The port number on which the Dogstatsd server is listening'),
            'maximum_packet_size':