class GrpcService(IOpenable, IConfigurable, IRegisterable, IUnreferenceable): """ Abstract service that receives remove calls via GRPC protocol. ### Configuration parameters ### - dependencies: - endpoint: override for GRPC Endpoint dependency - controller: override for Controller dependency - connection(s): - discovery_key: (optional) a key to retrieve the connection from :class:`IDiscovery <pip_services3_components.connect.IDiscovery.IDiscovery>` - protocol: connection protocol: http or https - host: host name or IP address - port: port number - uri: resource URI or connection string with all parameters in it - credential - the HTTPS credentials: - ssl_key_file: the SSL private key in PEM - ssl_crt_file: the SSL certificate in PEM - ssl_ca_file: the certificate authorities (root cerfiticates) in PEM .. code-block:: python class MyGrpcService(GrpcService): __controller: IMyController ... def __init__(self): suoer().__init__('... path to proto ...', '.. service name ...') self._dependency_resolver.put( "controller", Descriptor("mygroup","controller","*","*","1.0") ) def set_references(self, references): super().set_references(references) self._controller = this._dependency_resolver.get_required("controller") def register(self): def method(correlation_id, args, getted_method): correlationId = call.request.correlationId; id = call.request.id; self._controller.getMyData(correlationId, id, callback); self.register_commadable_method("get_mydata", None, method) ... service = MyGrpcService() service.configure(ConfigParams.from_tuples( "connection.protocol", "http", "connection.host", "localhost", "connection.port", 8080 )) service.set_references(References.from_tuples( Descriptor("mygroup","controller","default","default","1.0"), controller )) service.open("123") """ __default_config = ConfigParams.from_tuples("dependencies.endpoint", "*:endpoint:grpc:*:1.0") def __init__(self, service_name: str = None): """ Creates a new instance of the service. :param service_name: a service name. """ self.__config: ConfigParams = None self.__references: IReferences = None self.__local_endpoint: bool = None self.__implementation: Any = {} self.__interceptors: List[Any] = [] self.__opened: bool = None # The GRPC endpoint that exposes this service. self._endpoint: GrpcEndpoint = None # The dependency resolver. self._dependency_resolver = DependencyResolver( GrpcService.__default_config) # The logger. self._logger = CompositeLogger() # The performance counters. self._counters = CompositeCounters() self.__service_name = service_name self.__registrable = lambda implementation: self._register_service( implementation) def configure(self, config: ConfigParams): """ Configures component by passing configuration parameters. :param config: configuration parameters to be set. """ config = config.set_defaults(GrpcService.__default_config) self.__config = config self._dependency_resolver.configure(config) def set_references(self, references: IReferences): """ Sets references to this endpoint's logger, counters, and connection resolver. ### References ### - logger: **"\*:logger:\*:\*:1.0"** - counters: **"\*:counters:\*:\*:1.0"** - discovery: **"\*:discovery:\*:\*:1.0"** (for the connection resolver) :param references: an IReferences object, containing references to a logger, counters, and a connection resolver. """ self._logger.set_references(references) self._counters.set_references(references) self._dependency_resolver.set_references(references) # Get endpoint self._endpoint = self._dependency_resolver.get_one_optional('endpoint') # Or create a local one if self._endpoint is None: self._endpoint = self.__create_endpoint() self.__local_endpoint = True else: self.__local_endpoint = False # Add registration callback to the endpoint self._endpoint.register(self) def unset_references(self): """ Unsets (clears) previously set references to dependent components. """ # Remove registration callback from endpoint if self._endpoint is not None: self._endpoint.unregister(self.__registrable) self._endpoint = None def __create_endpoint(self) -> GrpcEndpoint: endpoint = GrpcEndpoint() if self.__config: endpoint.configure(self.__config) if self.__references: endpoint.set_references(self.__references) return endpoint def _instrument(self, correlation_id: Optional[str], name: str) -> CounterTiming: """ Adds instrumentation to log calls and measure call time. It returns a CounterTiming object that is used to end the time measurement. :param correlation_id: (optional) transaction id to trace execution through call chain. :param name: a method name. :return: CounterTiming object to end the time measurement. """ self._logger.trace(correlation_id, 'Executing {} method'.format(name)) self._counters.increment_one(name + '.exec_time') return self._counters.begin_timing(name + '.exec_time') def _instrument_error(self, correlation_id: Optional[str], name: str, err: Exception, reerror=False): """ Adds instrumentation to error handling. :param correlation_id: (optional) transaction id to trace execution through call chain. :param name: a method name. :param err: an occured error :param reerror: if true - throw error """ if err is not None: self._logger.error(correlation_id, err, 'Failed to execute {} method'.format(name)) self._counters.increment_one(name + '.exec_errors') if reerror: raise err def is_open(self) -> bool: """ Checks if the component is opened. :return: true if the component has been opened and false otherwise. """ return self.__opened def open(self, correlation_id: Optional[str]): """ Opens the component. :param correlation_id: (optional) transaction id to trace execution through call chain. """ # TODO maybe need add async if self.__opened: return None if self._endpoint is None: self._endpoint = self.__create_endpoint() self._endpoint.register(self) self.__local_endpoint = True if self.__local_endpoint: try: self._endpoint.open(correlation_id) self.__opened = True except Exception as ex: self.__opened = False raise ex else: self.__opened = True def close(self, correlation_id: Optional[str]): """ Closes component and frees used resources. :param correlation_id: (optional) transaction id to trace execution through call chain. """ if not self.__opened: return None if self._endpoint is None: raise InvalidStateException(correlation_id, 'NO_ENDPOINT', 'HTTP endpoint is missing') if self.__local_endpoint: self._endpoint.close(correlation_id) self.__opened = False def register_commadable_method( self, method: str, schema: Schema, action: Callable[[Optional[str], Optional[str], Parameters], None]): """ Registers a commandable method in this objects GRPC server (service) by the given name. :param method: the GRPC method name. :param schema: the schema to use for parameter validation. :param action: the action to perform at the given route. """ self._endpoint._register_commandable_method(method, schema, action) def _register_interceptor(self, action: Callable): """ Registers a middleware for methods in GRPC endpoint. :param action: an action function that is called when middleware is invoked. """ if self._endpoint is not None: self._endpoint._register_interceptor(action) def _register_service(self, implementation: 'GrpcService'): # self.register() if self._endpoint is not None: self._endpoint.register_service(implementation) @abstractmethod def register(self): """
class GrpcClient(IOpenable, IReferenceable, IConfigurable): """ Abstract client that calls remove endpoints using GRPC protocol. ### Configuration parameters ### - connection(s): - discovery_key: (optional) a key to retrieve the connection from :func:`link` - protocol: connection protocol: http or https - host: host name or IP address - port: port number - uri: resource URI or connection string with all parameters in it - options: - retries: number of retries (default: 3) - connect_timeout: connection timeout in milliseconds (default: 10 sec) - timeout: invocation timeout in milliseconds (default: 10 sec) .. code-block:: python class MyGrpcClient(GrpcClient, IMyClient): ... def get_data(self, correlation_id, id ): timing = self.instrument(correlation_id, 'myclient.get_data') result = self.call("get_data", correlation_id, { id: id }) timing.end_timing() return result ... client = MyGrpcClient() client.configure(ConfigParams.from_tuples( "connection.protocol", "http", "connection.host", "localhost", "connection.port", 8080 )) result = client.get_data("123", "1") """ _default_config = ConfigParams.from_tuples( "connection.protocol", "http", "connection.host", "0.0.0.0", "connection.port", 3000, "options.request_max_size", 1024 * 1024, "options.connect_timeout", 10000, "options.timeout", 10000, "options.retries", 3, "options.debug", True) def __init__(self, client_name: str): """ Creates a new instance of the client. :param client_name: a client name. """ self.__client = None self.__client_name = None # The GRPC client channel self._channel = None # The connection resolver. self._connection_resolver = HttpConnectionResolver() # The logger. self._logger = CompositeLogger() # The performance counters. self._counters = CompositeCounters() # The configuration options. self._options = ConfigParams() # The connection timeout in milliseconds. self._connection_timeout = 100000 # The invocation timeout in milliseconds. self._timeout = 100000 # The remote service uri which is calculated on open. self._uri: str = None self.__client_name = client_name def configure(self, config: ConfigParams): """ Configures component by passing configuration parameters. :param config: configuration parameters to be set. """ config = config.set_defaults(GrpcClient._default_config) self._connection_resolver.configure(config) self._options = self._options.override(config.get_section('options')) self._connection_timeout = config.get_as_integer_with_default( 'options.connect_timeout', self._connection_timeout) self._timeout = config.get_as_integer_with_default( 'options.timeout', self._timeout) def set_references(self, references: IReferences): """ Sets references to dependent components. :param references: references to locate the component dependencies. """ self._logger.set_references(references) self._counters.set_references(references) self._connection_resolver.set_references(references) def _instrument(self, correlation_id: Optional[str], name: str) -> CounterTiming: """ Adds instrumentation to log calls and measure call time. It returns a CounterTiming object that is used to end the time measurement. :param correlation_id: (optional) transaction id to trace execution through call chain. :param name: a method name. :return: CounterTiming object to end the time measurement. """ self._logger.trace(correlation_id, 'Executing {} method'.format(name)) self._counters.increment_one(name + '.call_count') return self._counters.begin_timing(name + '.call_time') def _instrument_error(self, correlation_id: Optional[str], name: str, err: Exception, reerror=False): """ Adds instrumentation to error handling. :param correlation_id: (optional) transaction id to trace execution through call chain. :param name: a method name. :param err: an occured error :param reerror: if true - throw error """ if err is not None: self._logger.error(correlation_id, err, 'Failed to call {} method'.format(name)) self._counters.increment_one(name + '.call_errors') if reerror is not None and reerror is True: raise err def is_open(self) -> bool: """ Checks if the component is opened. :return: Returns true if the component has been opened and false otherwise. """ return self._channel is not None def open(self, correlation_id: Optional[str]): """ Opens the component. :param correlation_id: (optional) transaction id to trace execution through call chain. """ if self.is_open(): return None try: connection = self._connection_resolver.resolve(correlation_id) self._uri = connection.get_as_string('uri') options = [('grpc.max_connection_idle_ms', self._connection_timeout), ('grpc.client_idle_timeout_ms', self._timeout)] if connection.get_as_string_with_default('protocol', 'http') == 'https': ssl_ca_file = connection.get_as_nullable_string('ssl_ca_file') with open(ssl_ca_file, 'rb') as file: trusted_root = file.read() credentials = grpc.ssl_channel_credentials(trusted_root) channel = grpc.secure_channel( str(connection.get_as_string('host')) + ':' + str(connection.get_as_string('port')), credentials=credentials, options=options) else: channel = grpc.insecure_channel( str(connection.get_as_string('host')) + ':' + str(connection.get_as_string('port')), options=options) self._channel = channel except Exception as ex: raise ConnectionException( correlation_id, 'CANNOT_CONNECT', 'Opening GRPC client failed').wrap(ex).with_details( 'url', self._uri) def close(self, correlation_id: Optional[str]): """ Closes component and frees used resources. :param correlation_id: (optional) transaction id to trace execution through call chain. """ if self._channel is not None: # Eat exceptions try: self._logger.debug( correlation_id, 'Closed GRPC service at {}'.format(self._uri)) except Exception as ex: self._logger.warn( correlation_id, 'Failed while closing GRPC service: {}'.format(ex)) # if self.__client is not None: # self.__client = None self._channel.close() self._channel = None self._uri = None GrpcClient._connection_resolver = HttpConnectionResolver() def call(self, method: str, client: Any, request: Any) -> Future: """ Calls a remote method via GRPC protocol. :param method: name of the calling method :param client: current client :param request: (optional) request object. :return: (optional) future that receives result object or error. """ client = client(self._channel) executor = futures.ThreadPoolExecutor(max_workers=1) response = executor.submit(client.__dict__[method], request) return response def _add_filter_params(self, params: Any, filter: Any) -> Any: """ AddFilterParams method are adds filter parameters (with the same name as they defined) to invocation parameter map. :param params: invocation parameters. :param filter: (optional) filter parameters :return: invocation parameters with added filter parameters. """ params = StringValueMap() if params is None else params if filter is not None: for k in filter.keys(): params.put(k, filter[k]) return params def _add_paging_params(self, params: Any, paging: Any) -> Any: """ AddPagingParams method are adds paging parameters (skip, take, total) to invocation parameter map. :param params: invocation parameters. :param paging: (optional) paging parameters :return: invocation parameters with added paging parameters. """ params = StringValueMap() if params is None else params if paging is not None: params.put('total', paging.total) if paging.skip is not None: params.put('skip', paging.skip) if paging.take is not None: params.put('take', paging.take) return params