def handle_exception(request, exception): exception_handler = utils.import_from_str( settings.get('EXCEPTION_HANDLER')) response = exception_handler(request, exception) # set exception to response convenience for `log_response` setattr(response, 'exception', exception) return response
async def start_mq_servers(sanic, loop): # init and start mq_server for server_name, params in settings.get('MQ_SERVERS').items(): server_cls = utils.import_from_str(params.pop('server')) if not issubclass(server_cls, BaseMqServer): raise Exception( f'{utils.cls_str_of_cls(server_cls)} is not subclass of {utils.cls_str_of_cls(BaseMqServer)}' ) server = server_cls(loop=loop, **params) registry.set(server_name, server, 'mq_servers') loop.create_task(server.start()) # subscribe handlers for server_name, handlers in registry.get_group( 'event_handlers').items(): server = registry.get(server_name, 'mq_servers') if not server: raise Exception(f'cannot find named "{server_name}" mq_server') for event_name, handler, msg_type, timeout, max_retry, subscribe in handlers: await server.subscribe(routing_key=event_name, handler=handler, msg_type=msg_type, timeout=timeout, max_retry=max_retry, subscribe=subscribe)
def patch(self, uri, host=None, strict_slashes=True, version=None, name=None, success_code=200, header_params: serializers.BaseSerializer = None, path_params: serializers.BaseSerializer = None, query_params: serializers.BaseSerializer = None, body_serializer: serializers.BaseSerializer = None, response_serializer: serializers.BaseSerializer = None, tags: list = None, context: dict = None, swagger_exclude: bool = False): context = context or {k: v for k, v in settings.get('DEFAULT_CONTEXT').items()} context['partial'] = True return self.wings_route(uri, method='PATCH', host=host, strict_slashes=strict_slashes, version=version, name=name, tags=tags, success_code=success_code, header_params=header_params, path_params=path_params, query_params=query_params, body_serializer=body_serializer, response_serializer=response_serializer, context=context, swagger_exclude=swagger_exclude)
def start(self): """ 启动 """ loop = registry.get('event_loop') or asyncio.get_event_loop() report_interval = settings.get('INSPECTOR_REPORT_INTERVAL') if self._count % report_interval == 0: logger.info('inspector working, count: %s', self._count) loop.call_later(self._interval, self.start) self._count += 1 if self._count > 9999999: self._count = 1 for task in self.tasks: func = task['func'] args = task['args'] if self._count % task['interval'] != 0 or task['times'] == 0: continue if asyncio.iscoroutinefunction(func): loop.create_task(func(*args)) else: loop.call_soon(func, *args) if task['times'] > 0: task['times'] -= 1
def exception_handler(request, exception): context = {k: v for k, v in settings.get('DEFAULT_CONTEXT').items()} response_shape = get_response_shape(context) result = utils.get_value(exception, 'message', str(exception)) code = utils.get_value(exception, 'status_code', 500) result, code = response_shape.create_body(result, code) return json(body=result, status=code)
def init_context(request): context_var.set({ 'trace_id': request.headers.get('X-TRACE-ID', '') or str(uuid.uuid4().hex), 'messages': [], 'request_at': int(datetime_helper.timestamp() * 1000) }) context_var.get().update(settings.get('DEFAULT_CONTEXT'))
async def __handle_message(self, channel, body, envelope, properties): encoding = properties.content_encoding or 'utf-8' content = body.decode(encoding) logger.debug('receive message from mq: %s', content) data = utils.instance_from_json(content) headers = utils.get_value(data, 'headers', {}) ctx = {'trace_id': utils.get_value(headers, 'X-TRACE-ID', '')} # 用户定制传递的消息头 for k in settings.get('CONTEXT_WHEN_DELIVERY'): h_k = 'X-' + k.upper().replace('_', '-') ctx[k] = utils.get_value(headers, h_k) context_var.set(ctx) message = utils.get_value(data, 'payload', data) message = utils.instance_from_json(message, cls=self.msg_type) retried_count = (properties.headers or {}).get('x-retry-count', 0) retried_count = retried_count if retried_count >= 0 else 0 try: if asyncio.iscoroutinefunction(self.handler): fu = asyncio.ensure_future(self.handler(message), loop=self.mq_server.loop) else: context = contextvars.copy_context() fu = self.mq_server.loop.run_in_executor( None, context.run, self.handler, message) await asyncio.wait_for(fu, self.timeout, loop=self.mq_server.loop) event.commit_events() logger.info( f'handle message success. event_name:{self.routing_key}, handler:{utils.meth_str(self.handler)}, ' f'retried_count: {retried_count}') except Exception as ex: if self.max_retry < 0 or retried_count < self.max_retry: await self.__publish_to_retry_queue(body, retried_count) error_info = f"event_name: {self.routing_key}, handler: {utils.meth_str(self.handler)}, " \ f"retried_count: {retried_count}, messages: \n{content}" logger.error(error_info, exc_info=ex) finally: await channel.basic_client_ack(envelope.delivery_tag) context_var.set(None)
def start(self): """ 启动 """ loop = registry.get('event_loop') or asyncio.get_event_loop() report_interval = settings.get('INSPECTOR_REPORT_INTERVAL') if self._count % report_interval == 0: wings_logger.info('inspector working, count: %s', self._count) loop.call_later(self._interval, self.start) self._count += 1 if self._count > 9999999: self._count = 1 def done_callback(future): from wings_sanic import event if not future.exception(): event.commit_events() context_var.set(None) for task in self.tasks: func = task['func'] args = task['args'] if self._count % task['interval'] != 0 or task['times'] == 0: continue context_var.set({ 'trace_id': str(uuid.uuid4().hex), 'messages': [] }) if asyncio.iscoroutinefunction(func): fu = asyncio.ensure_future(func(*args), loop=loop) else: context = contextvars.copy_context() fu = loop.run_in_executor(None, context.run, func, *args) fu.add_done_callback(done_callback) if task['times'] > 0: task['times'] -= 1
def wings_route(self, uri: str, method: str = 'GET', host=None, strict_slashes=True, version=None, name=None, success_code=200, header_params: serializers.BaseSerializer = None, path_params: serializers.BaseSerializer = None, query_params: serializers.BaseSerializer = None, body_serializer: serializers.BaseSerializer = None, response_serializer: serializers.BaseSerializer = None, tags: list = None, context: dict = None, swagger_exclude: bool = False): """ extension for Sanic's route. Contains parameters for wings framework. :param uri: the path for the handler to represent. :param host: :param strict_slashes: :param version: :param name: :param method: the method this function should respond to. Default GET. :param success_code: :param header_params: the arguments that should be passed into the header. :param path_params: the arguments that are specified by the path. By default, arguments that are found in the path are used first before the query_parameters and body_parameters. :param query_params: the arguments that should be query parameters. By default, all arguments are query_or path parameters for a GET request. :param body_serializer: the arguments that should be body parameters. By default, all arguments are either body or path parameters for a non-GET request. the whole body is validated against a single object. :param response_serializer: response spec :param tags: used for swagger tags :param context: maybe contains 'response_shape' to custom final response format. :param swagger_exclude: if True, Swagger Document will exclude this api. :return: decorated function """ method = method.upper() context = context or { k: v for k, v in settings.get('DEFAULT_CONTEXT').items() } metadata = HandlerMetaData(uri=uri, method=method, tags=tags, success_code=success_code, header_params=header_params, path_params=path_params, query_params=query_params, body_serializer=body_serializer, response_serializer=response_serializer, context=context, swagger_exclude=swagger_exclude) def decorator(raw_handler): @wraps(raw_handler) async def handler(request, *args, **kwargs): if isinstance(metadata.context, dict): from wings_sanic import context_var context_var.get().update(metadata.context) kwargs = await views.extract_params(request, metadata) result = await raw_handler(request, metadata=metadata, **kwargs) response = await views.process_result(result, metadata) return response handler.metadata = metadata self.add_route(handler=handler, uri=uri, methods=frozenset({method}), host=host, strict_slashes=strict_slashes, version=version, name=name) return handler return decorator
def build_spec(app, loop): for uri, route in app.router.routes_all.items(): if uri.startswith(f"{settings.GLOBAL_URL_PREFIX}/swagger"): continue # Build list of methods and their handler functions handler_type = type(route.handler) if handler_type is CompositionView: method_handlers = route.handler.handlers.items() else: method_handlers = zip(route.methods, repeat(route.handler)) # format path parameters '<param>' to '{param}' uri_parsed = uri for parameter in route.parameters: uri_parsed = re.sub('<' + parameter.name + '.*?>', '{' + parameter.name + '}', uri_parsed) for _method, _handler in method_handlers: metadata = utils.get_value(_handler, 'metadata') if _method == 'OPTIONS' or not metadata or metadata.swagger_exclude: continue # ensure group info group = metadata.swagger_group or 'default' if not isinstance(group, dict): group = {'title': str(group)} group_title = group.get('title', None) or 'default' if group_title not in _spec: _spec[group_title] = copy.deepcopy(settings.get('SWAGGER')) _spec[group_title].update({ 'swagger': '2.0', 'definitions': {}, 'paths': {}, }) _spec[group_title]['info'].update(group) group_spec = _spec[group_title] if uri_parsed not in group_spec['paths']: group_spec['paths'][uri_parsed] = {} # generate path parameters = [] # header for name, field in (utils.get_value(metadata.header_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'header'}) parameters.append(parameter) __update_definitions(group_spec, metadata.header_serializer) # path for name, field in (utils.get_value(metadata.path_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'path', 'required': True}) parameters.append(parameter) __update_definitions(group_spec, metadata.path_serializer) # query for name, field in (utils.get_value(metadata.query_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'query'}) parameters.append(parameter) __update_definitions(group_spec, metadata.query_serializer) # body has_file_field = False if metadata.body_serializer: body_spec = metadata.body_serializer.openapi_spec() import wings_sanic for _, f in metadata.body_serializer.fields.items(): if isinstance(f, wings_sanic.FileField): has_file_field = True break if not has_file_field: parameters.append({ 'in': 'body', 'name': 'body', 'required': True, 'schema': body_spec }) else: cls_str = utils.cls_str_of_obj(metadata.body_serializer) if cls_str in serializers.definitions: body_spec = serializers.definitions[cls_str] for name, field_spec in body_spec.get('properties', {}).items(): # 忽略read_only字段 if utils.get_value(field_spec, 'readOnly', False): continue parameters.append({ 'in': 'formData', 'name': name, 'required': name in utils.get_value(body_spec, 'required', []), **field_spec }) __update_definitions(group_spec, metadata.body_serializer) # response response_spec = None if metadata.response_serializer: response_spec = metadata.response_serializer.openapi_spec() __update_definitions(group_spec, metadata.response_serializer) response_shape = get_response_shape(metadata.context) response_spec = response_shape.swagger(response_spec) summary, description = __summary_description( inspect.cleandoc(_handler.__doc__ or "")) endpoint = { 'operationId': utils.meth_str(_handler), 'summary': summary, 'description': description, 'consumes': ['multipart/form-data'] if has_file_field else ['application/json'], 'produces': ['application/json'], 'tags': metadata.tags, 'parameters': parameters, 'responses': { "200": { "description": None, "examples": None, "schema": response_spec } }, } group_spec['paths'][uri_parsed][_method.lower()] = endpoint
def start(host='0.0.0.0', port=None, debug=False, ssl=None, sock=None, workers=None, protocol=None, backlog=100, stop_event=None, register_sys_signals=True, access_log=False, auto_reload=False, **kwargs): """ before start application, you need load your user_settings by `wings_sanic.settings.load(**user_settings)` :return: """ logging.config.dictConfig(settings.get('LOGGING_CONFIG')) if settings.get('CORS'): CORS(app, automatic_options=True, supports_credentials=True) app.config.update(settings.working_settings) try: import uvloop asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) except ImportError: pass # when DEBUG==True, swagger document is available if settings.get('DEBUG'): app.blueprint( WingsBluePrint.group(swagger_blueprint, url_prefix=settings.GLOBAL_URL_PREFIX)) @app.exception(NotFound) def handle_404_redirect(request, exception): import re if re.match('.*url.*not found', str(exception).lower()): return redirect(app.url_for('swagger.static')) return handle_exception(request, exception) bps = [ utils.import_from_str(bp_str) for bp_str in settings.get('BLUEPRINTS') ] app.blueprint( WingsBluePrint.group(bps, url_prefix=settings.GLOBAL_URL_PREFIX)) @app.exception(Exception) def handle_exception(request, exception): exception_handler = utils.import_from_str( settings.get('EXCEPTION_HANDLER')) response = exception_handler(request, exception) # set exception to response convenience for `log_response` setattr(response, 'exception', exception) return response @app.get(f'{settings.GLOBAL_URL_PREFIX}/ping/', response_serializer={ 'ping': serializers.StringField('Ping-Pong', required=True) }) async def ping(request, *args, **kwargs): return {'ping': 'pong'} @app.listener('after_server_start') async def inspector_start_working(sanic, loop): registry.set('event_loop', loop) loop.call_soon(inspector.start) @app.listener('after_server_start') async def start_mq_servers(sanic, loop): # init and start mq_server for server_name, params in settings.get('MQ_SERVERS').items(): server_cls = utils.import_from_str(params.pop('server')) if not issubclass(server_cls, BaseMqServer): raise Exception( f'{utils.cls_str_of_cls(server_cls)} is not subclass of {utils.cls_str_of_cls(BaseMqServer)}' ) server = server_cls(loop=loop, **params) registry.set(server_name, server, 'mq_servers') loop.create_task(server.start()) # subscribe handlers for server_name, handlers in registry.get_group( 'event_handlers').items(): server = registry.get(server_name, 'mq_servers') if not server: raise Exception(f'cannot find named "{server_name}" mq_server') for event_name, handler, msg_type, timeout, max_retry, subscribe in handlers: await server.subscribe(routing_key=event_name, handler=handler, msg_type=msg_type, timeout=timeout, max_retry=max_retry, subscribe=subscribe) app.run(host=host, port=port or settings.get('HTTP_PORT'), debug=debug, ssl=ssl, sock=sock, workers=workers or settings.get('WORKERS'), protocol=protocol, backlog=backlog, stop_event=stop_event, register_sys_signals=register_sys_signals, access_log=access_log, auto_reload=auto_reload, **kwargs)
def start(): """ before start application, you need load your user_settings by `wings_sanic.settings.load(**user_settings)` :return: """ if not settings.get('DEV'): logging.config.dictConfig(settings.get('LOG')) if settings.get('CORS'): CORS(app, automatic_options=True, supports_credentials=True) app.config.update(settings.working_settings) asyncio.set_event_loop_policy(uvloop.EventLoopPolicy()) # when DEBUG==True, swagger document is available if settings.get('DEBUG'): app.blueprint(swagger_blueprint) @app.exception(NotFound) def handle_404_redirect(request, exception): return redirect('/swagger/') for bp_str in settings.get('BLUEPRINTS'): bp = utils.import_from_str(bp_str) app.blueprint(bp) @app.middleware('response') def log_response(request, response): logger = logging.getLogger('wings_sanic') full_path = request.path + ('?%s' % request.query_string if request.query_string else '') request_url = '{0} {1}'.format(request.method, full_path) extra = {'remote_ip': request.remote_addr, 'request_uri': request_url} exception = getattr(response, 'exception', None) if exception: message = "request_url: {request_url}" \ "\nrequest_body: {body}" \ "\nstatus_code: {status_code}" \ "\nresponse_content: {response}" \ .format(request_url=request_url, body=request.body, response=response.body, status_code=response.status) if getattr(exception, 'code', 500) / 100 == 5: logger.error(message, extra=extra, exc_info=exception, stack_info=True) else: logger.warning(message, extra=extra, exc_info=exception) # 正常请求,记录少量信息 else: message = '%s %s' % (request_url, response.status) logger.info(message, extra=extra) @app.exception(Exception) def handle_exception(request, exception): exception_handler = utils.import_from_str( settings.get('EXCEPTION_HANDLER')) response = exception_handler(request, exception) # set exception to response convenience for `log_response` setattr(response, 'exception', exception) return response @app.get('/ping/', response_serializer={ 'ping': serializers.StringField('Ping-Pong', required=True) }) async def ping(request, *args, **kwargs): return {'ping': 'pong'} @app.listener('after_server_start') async def inspector_start_working(sanic, loop): registry.set('event_loop', loop) loop.call_soon(inspector.start) app.run(host="0.0.0.0", port=settings.get('HTTP_PORT'), workers=settings.get('WORKERS'), debug=settings.get('DEBUG'), access_log=settings.get('DEV'))
def build_spec(app, loop): _spec['swagger'] = '2.0' _spec.update(settings.get('SWAGGER')) # --------------------------------------------------------------- # # Paths # --------------------------------------------------------------- # paths = {} # tags = for uri, route in app.router.routes_all.items(): if uri.startswith("/swagger"): continue # Build list of methods and their handler functions handler_type = type(route.handler) if handler_type is CompositionView: method_handlers = route.handler.handlers.items() else: method_handlers = zip(route.methods, repeat(route.handler)) methods = {} for _method, _handler in method_handlers: metadata = _handler.metadata if _method == 'OPTIONS' or metadata.swagger_exclude: continue parameters = [] # header for name, field in (utils.get_value(metadata.header_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'header'}) parameters.append(parameter) # path for name, field in (utils.get_value(metadata.path_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'path', 'required': True}) parameters.append(parameter) # query for name, field in (utils.get_value(metadata.query_serializer, 'fields') or {}).items(): parameter = field.openapi_spec() parameter.update({'in': 'query'}) parameters.append(parameter) # body if metadata.body_serializer: spec = metadata.body_serializer.openapi_spec() parameters.append({ 'in': 'body', 'name': 'body', 'required': True, 'schema': spec }) # response response_spec = None if metadata.response_serializer: response_spec = metadata.response_serializer.openapi_spec() response_shape = get_response_shape(metadata.context) response_spec = response_shape.swagger(response_spec) summary, description = __summary_description( inspect.cleandoc(_handler.__doc__ or "")) endpoint = { 'operationId': utils.meth_str(_handler), 'summary': summary, 'description': description, 'consumes': ['application/json'], 'produces': ['application/json'], 'tags': metadata.tags, 'parameters': parameters, 'responses': { "200": { "description": None, "examples": None, "schema": response_spec } }, } methods[_method.lower()] = endpoint uri_parsed = uri for parameter in route.parameters: uri_parsed = re.sub('<' + parameter.name + '.*?>', '{' + parameter.name + '}', uri_parsed) paths[uri_parsed] = methods # --------------------------------------------------------------- # # Definitions # --------------------------------------------------------------- # _spec['definitions'] = { cls.__name__: definition for cls, (obj, definition) in serializers.definitions.items() } _spec['paths'] = paths