예제 #1
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)}
예제 #2
0
class ActionOne(Action):
    request_schema = fields.Dictionary({'planet': fields.UnicodeString()})

    response_schema = fields.Dictionary({
        'planet_response':
        fields.UnicodeString(),
        'settings':
        fields.Anything()
    })

    def run(self, request):
        return {
            'planet_response': request.body['planet'],
            'settings': self.settings
        }
예제 #3
0
class ClientSettings(SOASettings):
    """Generic settings for a Client."""
    schema = {
        'middleware':
        fields.List(
            BasicClassSchema(ClientMiddleware),
            description=
            'The list of all `ClientMiddleware` objects that should be applied to requests made from this '
            'client to the associated service',
        ),
        'transport':
        BasicClassSchema(BaseClientTransport),
        'transport_cache_time_in_seconds':
        fields.Anything(
            description=
            'This field is deprecated. The transport cache is no longer supported. This settings field '
            'will remain in place until 2018-06-15 to give a safe period for people to remove it from '
            'settings, but its value will always be ignored.', ),
    }
    defaults = {
        'transport_cache_time_in_seconds': 0,
    }
예제 #4
0
class ClassHoldingSigsToTest:
    def sig1(self, one, two=None):  # type: (AnyStr, Optional[bool]) -> None
        pass

    def sig1_35(self, one: AnyStr, two: Optional[bool] = None) -> None:  # noqa: E999
        pass

    def sig2(self, one, two=None, *args):
        # type: (bool, Optional[AnyStr], *int) -> List[int]
        pass

    def sig2_35(self, one: bool, *args: int, two: Optional[AnyStr] = None) -> List[int]:
        pass

    @decorated
    def sig3(
        self,
        one,
        two=None,
        **kwargs
    ):
        # type: (six.text_type, LocalToThisModuleOptionalInt, **bool) -> Dict[six.binary_type, int]
        pass

    @decorated
    @validate_method(fields.SchemalessDictionary(), fields.Anything())
    @decorated
    def sig3_super_wrapped(
        self,
        one,
        two=None,
        **kwargs
    ):
        # type: (six.text_type, LocalToThisModuleOptionalInt, **bool) -> Dict[six.binary_type, int]
        pass

    def sig3_35(
        self,
        one: six.text_type,
        *args: AnyType,
        two: Optional[int] = None,
        **kwargs: bool
    ) -> Dict[six.binary_type, int]:
        pass

    def sig4\
            (

                self,
                one,  # type: AnyStr
                two=None,  # type:  Optional[six.text_type]
                three=lambda x: True,  # type: Callable[[AnyStr], bool]
                *args,  # type: str
                **kwargs  # type: AnyType

            ):
        # type: (...) -> six.binary_type
        pass

    def sig4_35(
        self,
        one: AnyStr,
        two: Optional[six.text_type],
        three: Callable[[AnyStr], bool] = lambda x: True,
        *args: str,
        **kwargs: AnyType
    ) -> bytes:
        pass
예제 #5
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(),
    }
예제 #6
0
class IntrospectionAction(Action):
    """
    This action returns detailed information about the service's defined actions and the request and response schemas
    for each action, along with any documentation defined for the action or for the service itself. It can be passed
    a single action name to return information limited to that single action. Otherwise, it will return information for
    all of the service's actions.

    This action will be added to your service on your behalf if you do not define an action with name `introspect`.

    Making your services and actions capable of being introspected is simple. If your server class has a `description`
    attribute, that will be the service's documentation that introspection returns. If your server class does not have
    this attribute but does have a docstring, introspection will use the docstring. The same rule applies to action
    classes: Introspection first looks for a `description` attribute and then uses the docstring, if any. If neither of
    these are found, the applicable service or action documentation will be done.

    Introspection then looks at the `request_schema` and `response_schema` attributes for each of your actions, and
    includes the details about these schemas in the returned information for each action. Be sure you include field
    descriptions in your schema for the most effective documentation possible.
    """
    description = (
        "This action returns detailed information about the service's defined actions and the request and response "
        "schemas for each action, along with any documentation defined for the action or for the service itself. It "
        "can be passed a single action name to return information limited to that single action. Otherwise, it will "
        "return information for all of the service's actions. If an action is a switched action (meaning the action "
        "extends `SwitchedAction`, and which action code runs is controlled with SOA switches), multiple action "
        "introspection results will be returned for that action, each with a name ending in either `[switch:N]` (where "
        "`N` is the switch value) or `[DEFAULT]` for the default action."
    )

    request_schema = fields.Dictionary(
        {
            'action_name': fields.UnicodeString(
                min_length=1,
                allow_blank=False,
                description='Specify this to limit your introspection to a single action. It will be the only action '
                            'present in the `actions` response attribute. If the requested action does not exist, an '
                            'error will be returned.',
            ),
        },
        optional_keys=('action_name', ),
    )

    response_schema = fields.Dictionary(
        {
            'documentation': fields.Nullable(fields.UnicodeString(
                description='The documentation for the server, unless `action_name` is specified in the request body, '
                            'in which case this is omitted.',
            )),
            'action_names': fields.List(
                fields.UnicodeString(description='The name of an action.'),
                description='An alphabetized list of every action name included in `actions`.',
            ),
            'actions': fields.SchemalessDictionary(
                key_type=fields.UnicodeString(description='The name of the action.'),
                value_type=fields.Dictionary(
                    {
                        'documentation': fields.Nullable(
                            fields.UnicodeString(description='The documentation for the action'),
                        ),
                        'request_schema': fields.Nullable(fields.Anything(
                            description='A description of the expected request schema, including any documentation '
                                        'specified in the schema definition.',
                        )),
                        'response_schema': fields.Nullable(fields.Anything(
                            description='A description of the guaranteed response schema, including any documentation '
                                        'specified in the schema definition.',
                        )),
                    },
                    description='A introspection of a single action',
                ),
                description='A dict mapping action names to action description dictionaries. This contains details '
                            'about every action in the service unless `action_name` is specified in the request body, '
                            'in which case it contains details only for that action.',
            ),
        },
        optional_keys=('documentation', ),
    )

    def __init__(self, server):
        """
        Construct a new introspection action. Unlike its base class, which accepts a server settings object, this
        must be passed a `Server` object, from which it will obtain a settings object. The `Server` code that calls
        this action has special handling to address this requirement.

        :param server: A PySOA server instance
        :type server: Server
        """
        if not isinstance(server, Server):
            raise TypeError('First argument (server) must be a Server instance')

        super(IntrospectionAction, self).__init__(server.settings)

        self.server = server

    def run(self, request):
        """
        Introspects all of the actions on the server and returns their documentation.

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

        :return: The response
        """
        if request.body.get('action_name'):
            return self._get_response_for_single_action(request.body.get('action_name'))

        return self._get_response_for_all_actions()

    def _get_response_for_single_action(self, request_action_name):
        action_name = request_action_name
        switch = None

        if SWITCHED_ACTION_RE.match(action_name):
            match = SWITCHED_ACTION_RE.match(action_name)
            action_name = match.group(str('action'))
            if match.group(str('default')):
                switch = SwitchedAction.DEFAULT_ACTION
            else:
                switch = int(match.group(str('switch')))

        if action_name not in self.server.action_class_map and action_name not in ('status', 'introspect'):
            raise ActionError(errors=[
                Error(code=ERROR_CODE_INVALID, message='Action not defined in service', field='action_name'),
            ])

        if action_name in self.server.action_class_map:
            action_class = self.server.action_class_map[action_name]
            if issubclass(action_class, SwitchedAction):
                if switch:
                    if switch == SwitchedAction.DEFAULT_ACTION:
                        action_class = action_class.switch_to_action_map[-1][1]
                    else:
                        for matching_switch, action_class in action_class.switch_to_action_map:
                            if switch == matching_switch:
                                break
                else:
                    response = {
                        'action_names': [],
                        'actions': {}
                    }
                    for sub_name, sub_class in self._iterate_switched_actions(action_name, action_class):
                        response['action_names'].append(sub_name)
                        response['actions'][sub_name] = self._introspect_action(sub_class)
                    response['action_names'] = list(sorted(response['action_names']))
                    return response
        elif action_name == 'introspect':
            action_class = self.__class__
        else:
            action_class = BaseStatusAction

        return {
            'action_names': [request_action_name],
            'actions': {request_action_name: self._introspect_action(action_class)}
        }

    def _get_response_for_all_actions(self):
        response = {
            'actions': {},
            'action_names': [],
            'documentation': getattr(self.server.__class__, 'description', self.server.__class__.__doc__) or None,
        }

        if 'introspect' not in self.server.action_class_map:
            response['action_names'].append('introspect')
            response['actions']['introspect'] = self._introspect_action(self.__class__)

        if 'status' not in self.server.action_class_map:
            response['action_names'].append('status')
            response['actions']['status'] = self._introspect_action(BaseStatusAction)

        for action_name, action_class in six.iteritems(self.server.action_class_map):
            if issubclass(action_class, SwitchedAction):
                for sub_action_name, sub_action_class in self._iterate_switched_actions(action_name, action_class):
                    response['action_names'].append(sub_action_name)
                    response['actions'][sub_action_name] = self._introspect_action(sub_action_class)
            else:
                response['action_names'].append(action_name)
                response['actions'][action_name] = self._introspect_action(action_class)

        response['action_names'] = list(sorted(response['action_names']))

        return response

    @staticmethod
    def _iterate_switched_actions(action_name, action_class):
        found_default = False
        last_index = len(action_class.switch_to_action_map) - 1
        for i, (switch, sub_action_class) in enumerate(action_class.switch_to_action_map):
            if switch == SwitchedAction.DEFAULT_ACTION:
                sub_action_name = '{}[DEFAULT]'.format(action_name)
                found_default = True
            elif not found_default and i == last_index:
                sub_action_name = '{}[DEFAULT]'.format(action_name)
            else:
                sub_action_name = '{}[switch:{}]'.format(action_name, get_switch(switch))

            yield sub_action_name, sub_action_class

    @staticmethod
    def _introspect_action(action_class):
        action = {
            'documentation': getattr(action_class, 'description', action_class.__doc__) or None,
            'request_schema': None,
            'response_schema': None,
        }

        if getattr(action_class, 'request_schema', None):
            action['request_schema'] = action_class.request_schema.introspect()

        if getattr(action_class, 'response_schema', None):
            action['response_schema'] = action_class.response_schema.introspect()

        return action