-
Notifications
You must be signed in to change notification settings - Fork 148
Add an HTTP client for service invocation and default to using it #182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| Copyright (c) Microsoft Corporation. | ||
| Licensed under the MIT License. | ||
| """ | ||
|
|
||
| import aiohttp | ||
|
|
||
| from typing import Dict, Optional, Union, Tuple, TYPE_CHECKING | ||
| if TYPE_CHECKING: | ||
| from dapr.serializers import Serializer | ||
|
|
||
| from dapr.conf import settings | ||
| from dapr.clients.base import DEFAULT_JSON_CONTENT_TYPE | ||
| from dapr.clients.exceptions import DaprInternalError, ERROR_CODE_DOES_NOT_EXIST, ERROR_CODE_UNKNOWN | ||
| from opencensus.trace.tracers.base import Tracer # type: ignore | ||
|
|
||
| CONTENT_TYPE_HEADER = 'content-type' | ||
| DAPR_API_TOKEN_HEADER = 'dapr-api-token' | ||
|
|
||
|
|
||
| class DaprHttpClient: | ||
| """A Dapr Http API client""" | ||
|
|
||
| def __init__(self, | ||
| message_serializer: 'Serializer', | ||
| timeout: int = 60, | ||
| tracer: Optional[Tracer] = None): | ||
|
|
||
| self._timeout = aiohttp.ClientTimeout(total=timeout) | ||
| self._serializer = message_serializer | ||
| self._tracer = tracer | ||
|
|
||
| def get_api_url(self) -> str: | ||
| return 'http://{}:{}/{}'.format( | ||
| settings.DAPR_RUNTIME_HOST, | ||
| settings.DAPR_HTTP_PORT, | ||
| settings.DAPR_API_VERSION) | ||
|
|
||
| async def send_bytes( | ||
| self, method: str, url: str, | ||
| data: Optional[bytes], | ||
| headers: Dict[str, Union[bytes, str]] = {}, | ||
| query_params: Dict[str, Union[bytes, str]] = {} | ||
| ) -> Tuple[bytes, aiohttp.ClientResponse]: | ||
| if not headers.get(CONTENT_TYPE_HEADER): | ||
| headers[CONTENT_TYPE_HEADER] = DEFAULT_JSON_CONTENT_TYPE | ||
|
|
||
| if settings.DAPR_API_TOKEN is not None: | ||
| headers[DAPR_API_TOKEN_HEADER] = settings.DAPR_API_TOKEN | ||
|
|
||
| if self._tracer is not None: | ||
| trace_headers = self._tracer.propagator.to_headers(self._tracer.span_context) | ||
| headers.update(trace_headers) | ||
|
|
||
| r = None | ||
| async with aiohttp.ClientSession(timeout=self._timeout) as session: | ||
| r = await session.request( | ||
| method=method, | ||
| url=url, | ||
| data=data, | ||
| headers=headers, | ||
| params=query_params) | ||
|
|
||
| if r.status >= 200 and r.status < 300: | ||
| return await r.read(), r | ||
|
|
||
| raise (await self.convert_to_error(r)) | ||
|
|
||
| async def convert_to_error(self, response) -> DaprInternalError: | ||
| error_info = None | ||
| try: | ||
| error_body = await response.read() | ||
| if (error_body is None or len(error_body) == 0) and response.status == 404: | ||
| return DaprInternalError("Not Found", ERROR_CODE_DOES_NOT_EXIST) | ||
| error_info = self._serializer.deserialize(error_body) | ||
| except Exception: | ||
| return DaprInternalError(f'Unknown Dapr Error. HTTP status code: {response.status}') | ||
|
|
||
| if error_info and isinstance(error_info, dict): | ||
| message = error_info.get('message') | ||
| error_code = error_info.get('errorCode') or ERROR_CODE_UNKNOWN | ||
| return DaprInternalError(message, error_code) | ||
|
|
||
| return DaprInternalError(f'Unknown Dapr Error. HTTP status code: {response.status}') | ||
|
wcs1only marked this conversation as resolved.
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,76 @@ | ||
| # -*- coding: utf-8 -*- | ||
|
|
||
| """ | ||
| Copyright (c) Microsoft Corporation. | ||
| Licensed under the MIT License. | ||
| """ | ||
|
|
||
| import asyncio | ||
|
|
||
| from typing import Optional, Union | ||
|
|
||
| from dapr.clients.http.client import DaprHttpClient, CONTENT_TYPE_HEADER | ||
| from dapr.clients.grpc._helpers import MetadataTuple, GrpcMessage | ||
| from dapr.clients.grpc._response import InvokeMethodResponse | ||
| from dapr.serializers import DefaultJSONSerializer | ||
| from opencensus.trace.tracers.base import Tracer # type: ignore | ||
|
|
||
|
|
||
| class DaprInvocationHttpClient: | ||
| """Service Invocation HTTP Client""" | ||
|
|
||
| def __init__(self, timeout: int = 60, tracer: Optional[Tracer] = None): | ||
| self._client = DaprHttpClient(DefaultJSONSerializer(), timeout, tracer) | ||
|
|
||
| def invoke_method( | ||
| self, | ||
| app_id: str, | ||
| method_name: str, | ||
| data: Union[bytes, str, GrpcMessage], | ||
| content_type: Optional[str] = None, | ||
| metadata: Optional[MetadataTuple] = None, | ||
| http_verb: Optional[str] = None, | ||
| http_querystring: Optional[MetadataTuple] = None) -> InvokeMethodResponse: | ||
| """This implementation is designed to seemlessly mimic the behavior of the grpc | ||
| service invoke implementation. Please see the grpc version for parameter definitions | ||
| and samples""" | ||
|
|
||
| verb = 'GET' | ||
| if http_verb is not None: | ||
| verb = http_verb | ||
|
|
||
| headers = {} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need DAPR_API_TOKEN and the traceparent header to be set here.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ended up putting this in dapr/clients/http/client.py:50 so it can be shared with the Actor API client. |
||
| if metadata is not None: | ||
| for key, value in metadata: | ||
| headers[key] = value | ||
| query_params = {} | ||
| if http_querystring is not None: | ||
| for key, value in http_querystring: | ||
| query_params[key] = value | ||
|
|
||
| if content_type is not None: | ||
| headers[CONTENT_TYPE_HEADER] = content_type | ||
|
|
||
| url = f'{self._client.get_api_url()}/invoke/{app_id}/method/{method_name}' | ||
|
|
||
| if isinstance(data, GrpcMessage): | ||
| body = data.SerializeToString() | ||
| elif isinstance(data, str): | ||
| body = data.encode('utf-8') | ||
| else: | ||
| body = data | ||
|
|
||
| async def make_request(): | ||
| resp_body, r = await self._client.send_bytes( | ||
| method=verb, | ||
| headers=headers, | ||
| url=url, | ||
| data=body, | ||
| query_params=query_params) | ||
|
|
||
| resp_data = InvokeMethodResponse(resp_body, r.content_type) | ||
| for key in r.headers: | ||
| resp_data.headers[key] = r.headers.getall(key) # type: ignore | ||
| return resp_data | ||
|
|
||
| return asyncio.run(make_request()) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why renaming it if it is in another package?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because there is a new DaprClient which inherits from this class. Would be confusing if I named it the same, wouldn't it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
You can name it differently when importing it. It is already in another package.
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, but we only named it so generically in the first place because we only had the gRPC client and nothing else. Now that we have both gRPC and HTTP, naming them more specifically makes sense.