-
-
Notifications
You must be signed in to change notification settings - Fork 31.4k
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
async HTTP component #3914
async HTTP component #3914
Conversation
@balloob, thanks for your PR! By analyzing the history of the files in this pull request, we identified @fabaff, @robbiet480 and @JshWright to be potential reviewers. |
|
||
|
||
class APIEntityStateView(HomeAssistantView): | ||
"""View to handle EntityState requests.""" | ||
|
||
url = "/api/states/<entity(exist=False):entity_id>" | ||
url = "/api/states/{entity_id}" # TODO validation <entity(exist=False):entity_id> |
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.
- 80: E501 line too long (86 > 79 characters)
def stop_wsgi_server(event): | ||
"""Stop the WSGI server.""" | ||
server.stop() | ||
def start_server(event): |
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.
- 5: F811 redefinition of unused 'start_server' from line 138
# if cors_check: | ||
# response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_ORIGIN] = \ | ||
# environ.get('HTTP_ORIGIN') | ||
# response.headers[HTTP_HEADER_ACCESS_CONTROL_ALLOW_HEADERS] = \ |
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.
- 80: E501 line too long (80 > 79 characters)
# TODO CORS ? | ||
return web.Response('', status=200) | ||
|
||
def __call__(self, 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.
- D102: Missing docstring in public method
I am planning on adding aiohttp to the core dependencies, just like requests is. |
def stop_wsgi_server(event): | ||
"""Stop the WSGI server.""" | ||
server.stop() | ||
def start_server(event): |
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.
- 5: F811 redefinition of unused 'start_server' from line 137
|
||
@asyncio.coroutine |
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.
- 5: E303 too many blank lines (2)
|
||
from homeassistant.core import callback, is_callback |
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.
- 1: F401 'callback' imported but unused
|
||
if FFMPEG_CONFIG.get(CONF_RUN_TEST): | ||
# if in cache | ||
if input_source in FFMPEG_TEST_CACHE: | ||
return FFMPEG_TEST_CACHE[input_source] | ||
|
||
# run test | ||
test = Test(get_binary()) | ||
if not test.run_test(input_source): | ||
ffmpeg_test = Test(get_binary()) |
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.
- 23: F821 undefined name 'Test'
raise BadRequest('Unable to read JSON request') | ||
|
||
return Request | ||
class GzipFileSender(FileSender): |
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.
- D204: 1 blank line required after class docstring (found 0)
Besides the camera view, this is ready for review. @bbangert, when you have time, could you take a look. Thanks. |
break | ||
response.write(data) | ||
# pylint: disable=broad-except | ||
except Exception as err: |
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.
- 13: F841 local variable 'err' is assigned to but never used
response.write(data) | ||
# pylint: disable=broad-except | ||
except Exception as err: | ||
_LOGGER.debug("Exception on stream sending.", exc_info=True) |
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.
- 17: F821 undefined name '_LOGGER'
"""Stream images as mjpeg stream.""" | ||
img_bytes = yield from self.entity.async_camera_image() | ||
|
||
if img_bytes is not None and img_bytes != last_image: |
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.
- 51: F821 undefined name 'last_image'
img_bytes = yield from self.entity.async_camera_image() | ||
|
||
if img_bytes is not None and img_bytes != last_image: | ||
img_bytes = bytes( |
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.
- 24: E222 multiple spaces after operator
response.write(data) | ||
# pylint: disable=broad-except | ||
except Exception: | ||
_LOGGER.debug("Exception on stream sending.", exc_info=True) |
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.
- 17: F821 undefined name '_LOGGER'
# connect to stream | ||
try: | ||
with async_timeout.timeout(10, loop=self.hass.loop): | ||
stream = yield from session.get(self._mjpeg_url) |
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.
- 37: F821 undefined name 'session'
else: | ||
self._auth = None | ||
|
||
self._last_url = None | ||
self._last_image = None | ||
self._session = aiohttp.ClientSession(loop=hass.loop, auth=auth) |
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.
- 68: F821 undefined name 'auth'
|
||
def camera_image(self): | ||
"""Return bytes of camera image.""" | ||
return run_coroutine_threadsafe( |
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.
- 16: F821 undefined name 'run_coroutine_threadsafe'
@@ -4,10 +4,14 @@ | |||
For more details about this platform, please refer to the documentation at | |||
https://home-assistant.io/components/camera.generic/ | |||
""" | |||
import asyncio | |||
import functools as ft |
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.
- 1: F401 'ft' imported but unused
So my work on camera component and his platform is done 💯 |
return self._requests[key] | ||
|
||
|
||
class AiohttpClientMockResponse: |
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.
- D204: 1 blank line required after class docstring (found 0)
|
||
class AiohttpClientMockResponse: | ||
"""Mock Aiohttp client response.""" | ||
def __init__(self, method, url, status, response): |
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.
- D102: Missing docstring in public method
|
||
|
||
@pytest.fixture | ||
def aioclient_mock(): |
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.
- D202: No blank lines allowed after function docstring (found 1)
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.
Looks good overall. Left a few questions that may or may not be actual issues, hard to hold all the context at once. It might be nice to clarify whether async_ means its async safe, or is actually a @callback.
|
||
return self.json_message("Event forwarding setup.") | ||
|
||
@asyncio.coroutine | ||
def delete(self, request): | ||
"""Remove event forwarer.""" |
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.
forwarder perhaps?
@@ -364,6 +398,7 @@ class APIErrorLogView(HomeAssistantView): | |||
url = URL_API_ERROR_LOG | |||
name = "api:error-log" | |||
|
|||
@asyncio.coroutine | |||
def get(self, request): | |||
"""Serve error log.""" | |||
return self.file(request, self.hass.config.path(ERROR_LOG_FILENAME)) |
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.
A file request that doesn't block?
|
||
def write(img_bytes): | ||
"""Write image to stream.""" | ||
response.write(bytes( |
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.
Nonblocking call?
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.
It's nonblocking: http://aiohttp.readthedocs.io/en/stable/web_reference.html#aiohttp.web.StreamResponse.write
Instead, we yield from response.drain()
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.
Just read 2 comments below 👍 we're all good
break | ||
|
||
if img_bytes is not None and img_bytes != last_image: | ||
write(img_bytes) |
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.
I would've expected a yield to be able to write to a socket under async.
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.
See above.
write(img_bytes) | ||
|
||
last_image = img_bytes | ||
yield from response.drain() |
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.
If this is the call that waits for a write buffer to empty, ignore the above comments about waiting on the write.
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.
I should have read all the comments before commenting 🎉
@@ -193,9 +201,10 @@ def __init__(self, hass, extra_urls): | |||
) | |||
) | |||
|
|||
@asyncio.coroutine |
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.
A coroutine but nothing needs to yield?
@@ -230,7 +239,7 @@ def get(self, request, entity_id=None): | |||
icons_url=icons_url, icons=FINGERPRINTS['mdi.html'], |
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.
async_render?
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.
This is a jinja2 template instance, not a HASS instance.
|
||
return parsed | ||
finally: | ||
resp.set_tcp_nodelay(True) |
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.
Where is it set to False?
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.
This code is a direct copy of the aiohttp code. I am planning on contributing it back to them. For now this is a direct copy of their code.
set_tcp_nodelay
will reverse the set_tcp_cork
call a few lines above. Docs.
@@ -100,14 +102,14 @@ def get(self, request): | |||
elif is_value: | |||
pid = convert_pid(is_value.group(1)) | |||
if pid in self.sensors: | |||
self.sensors[pid].on_update(data[key]) | |||
self.sensors[pid].async_on_update(data[key]) |
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.
Guaranteed to not be a coroutine?
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.
Yes
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.
I've added a callback annotation to the method for clarity
|
||
def disconnect(self, api): | ||
def async_disconnect(self, api): |
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.
Are these callbacks or safe from to call from any thread?
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.
Callbacks. Added annotation.
They are only called from the APi when setting up/deleting event forwarding.
@bbangert you found more than I would have expected. It's all fixed now 👯♂️ |
I have been working on porting over the HTTP component to use aiohttp instead of CherryPy WSGI server + Werkzeug. Once this is done, adding a websocket server is pretty much free too 👍
I had to enhance some internal aiohttp classes to get gzip / md5 fingerprinting / caching working. I am planning to see if I can get support for this merged upstream.
Features to add back:
Components to fix: