You've already forked homehub
Release 1.3.0
This commit is contained in:
6
packages/homehub_backend/.gitignore
vendored
Normal file
6
packages/homehub_backend/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
__pycache__/
|
||||
build/
|
||||
dist/
|
||||
homehub_backend.egg-info/
|
||||
|
||||
*.pyc
|
||||
201
packages/homehub_backend/LICENSE.txt
Normal file
201
packages/homehub_backend/LICENSE.txt
Normal file
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
1
packages/homehub_backend/MANIFEST.in
Normal file
1
packages/homehub_backend/MANIFEST.in
Normal file
@@ -0,0 +1 @@
|
||||
include LICENSE.txt NOTICE.txt README.md requirements.txt
|
||||
18
packages/homehub_backend/Makefile
Normal file
18
packages/homehub_backend/Makefile
Normal file
@@ -0,0 +1,18 @@
|
||||
PHONY: clean dev lint test build publish
|
||||
|
||||
clean:
|
||||
rm -rf build/ dist/ homehub_backend-egg-info/
|
||||
|
||||
dev:
|
||||
|
||||
lint:
|
||||
flake8
|
||||
|
||||
test:
|
||||
pytest -v --disable-warnings
|
||||
|
||||
build: clean
|
||||
python setup.py build sdist bdist_wheel
|
||||
|
||||
publish:
|
||||
twine upload --repository pypi-hosted.nexus.bthlabs.pl --skip-existing dist/*
|
||||
14
packages/homehub_backend/NOTICE.txt
Normal file
14
packages/homehub_backend/NOTICE.txt
Normal file
@@ -0,0 +1,14 @@
|
||||
BTHLabs HomeHub - Backend Application
|
||||
Copyright 2021-present BTHLabs <contact@bthlabs.pl> (https://bthlabs.pl/)
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
3
packages/homehub_backend/README.md
Normal file
3
packages/homehub_backend/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# homehub_backend
|
||||
|
||||
BTHLabs HomeHub - Backend Application
|
||||
3
packages/homehub_backend/homehub_backend/__init__.py
Normal file
3
packages/homehub_backend/homehub_backend/__init__.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
__version__ = '1.3.0'
|
||||
82
packages/homehub_backend/homehub_backend/app.py
Normal file
82
packages/homehub_backend/homehub_backend/app.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import datetime
|
||||
import importlib
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from aiohttp import web
|
||||
from aiojobs.aiohttp import setup as setup_aiojobs
|
||||
|
||||
from homehub_backend import handlers
|
||||
from homehub_backend.defs import FRONTEND_DIR
|
||||
from homehub_backend.lib.state_store import setup as setup_state_store
|
||||
from homehub_backend.lib.rpc import setup as setup_rpc
|
||||
from homehub_backend.lib.services import setup as setup_services
|
||||
from homehub_backend.lib.websocket import setup as setup_websocket
|
||||
|
||||
settings = importlib.import_module(
|
||||
os.environ.get('HOMEHUB_SETTINGS_MODULE', 'settings'),
|
||||
)
|
||||
|
||||
logger = logging.getLogger('homehub')
|
||||
if getattr(settings, 'TESTING', False):
|
||||
logger.setLevel(logging.WARNING)
|
||||
elif settings.DEBUG:
|
||||
logger.setLevel(logging.DEBUG)
|
||||
else:
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
handler = logging.StreamHandler(sys.stdout)
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s %(name)s: %(levelname)s: %(message)s',
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
logger.addHandler(handler)
|
||||
|
||||
app_logger = logging.getLogger('homehub.app')
|
||||
|
||||
|
||||
async def app_on_startup(app):
|
||||
logger.info('HomeHub 1.0 by BTHLabs')
|
||||
if app['SETTINGS'].DEBUG:
|
||||
logger.debug('Running with DEBUG=True')
|
||||
|
||||
app['STARTUP_TS'] = datetime.datetime.now()
|
||||
|
||||
logger.debug('My PID = {pid}'.format(pid=os.getpid()))
|
||||
|
||||
|
||||
def create_app(loop=None):
|
||||
app = web.Application(logger=app_logger, loop=loop)
|
||||
app['SETTINGS'] = settings
|
||||
|
||||
app.on_startup.append(app_on_startup)
|
||||
|
||||
setup_aiojobs(app)
|
||||
setup_rpc(app)
|
||||
setup_state_store(app)
|
||||
setup_services(app)
|
||||
setup_websocket(app)
|
||||
|
||||
frontend_dir = getattr(settings, 'FRONTEND_DIR', FRONTEND_DIR)
|
||||
index_html_path = os.path.join(frontend_dir, 'index.html')
|
||||
if os.path.isfile(index_html_path):
|
||||
with codecs.open(index_html_path, 'r', 'utf-8') as index_html_f:
|
||||
app['INDEX_HTML'] = index_html_f.read()
|
||||
|
||||
app.add_routes([
|
||||
web.get('/', handlers.get_index),
|
||||
web.get('/index.html', handlers.get_index),
|
||||
])
|
||||
|
||||
if os.path.isdir(frontend_dir):
|
||||
app.add_routes([
|
||||
web.static('/frontend', frontend_dir),
|
||||
])
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
@@ -0,0 +1,2 @@
|
||||
from .paths import * # noqa: F401, F403
|
||||
from .services import * # noqa: F401, F403
|
||||
5
packages/homehub_backend/homehub_backend/defs/paths.py
Normal file
5
packages/homehub_backend/homehub_backend/defs/paths.py
Normal file
@@ -0,0 +1,5 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
CONTEXT = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
|
||||
FRONTEND_DIR = os.path.join(os.getcwd(), 'frontend')
|
||||
@@ -0,0 +1,2 @@
|
||||
kServiceUptime = 'kServiceUptime'
|
||||
kServiceWeather = 'kServiceWeather'
|
||||
@@ -0,0 +1 @@
|
||||
from .main import * # noqa: F401, F403
|
||||
20
packages/homehub_backend/homehub_backend/handlers/main.py
Normal file
20
packages/homehub_backend/homehub_backend/handlers/main.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from aiohttp import web
|
||||
|
||||
__all__ = ['get_index']
|
||||
|
||||
|
||||
async def get_index(request):
|
||||
index_html = request.config_dict.get('INDEX_HTML', None)
|
||||
if not index_html:
|
||||
return web.Response(
|
||||
content_type='text/plain',
|
||||
charset='utf-8',
|
||||
text='404: Not Found',
|
||||
)
|
||||
|
||||
return web.Response(
|
||||
content_type='text/html',
|
||||
charset='utf-8',
|
||||
text=index_html,
|
||||
)
|
||||
175
packages/homehub_backend/homehub_backend/lib/rpc.py
Normal file
175
packages/homehub_backend/homehub_backend/lib/rpc.py
Normal file
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import logging
|
||||
from types import MethodType
|
||||
|
||||
from aiohttp import web
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.rpc')
|
||||
|
||||
|
||||
class BaseJSONRPCError(Exception):
|
||||
def __init__(self, data=None):
|
||||
self.data = data
|
||||
|
||||
def to_json(self):
|
||||
result = {
|
||||
'code': self.ERROR_CODE,
|
||||
'message': self.ERROR_MESSAGE,
|
||||
}
|
||||
|
||||
if self.data:
|
||||
result['data'] = self.data
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class JSONRPCParseError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32700
|
||||
ERROR_MESSAGE = 'Parse error'
|
||||
|
||||
|
||||
class JSONRPCInvalidRequestError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32600
|
||||
ERROR_MESSAGE = 'Invalid Request'
|
||||
|
||||
|
||||
class JSONRPCMethodNotFoundError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32601
|
||||
ERROR_MESSAGE = 'Method not found'
|
||||
|
||||
|
||||
class JSONRPCInvalidParamsError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32602
|
||||
ERROR_MESSAGE = 'Invalid params'
|
||||
|
||||
|
||||
class JSONRPCInternalErrorError(BaseJSONRPCError):
|
||||
ERROR_CODE = -32603
|
||||
ERROR_MESSAGE = 'Internal error'
|
||||
|
||||
|
||||
async def post_rpc(request):
|
||||
responses = []
|
||||
results = []
|
||||
calls = []
|
||||
try:
|
||||
try:
|
||||
data = await request.json()
|
||||
except Exception as exc:
|
||||
LOGGER.error('Error deserializing RPC call!', exc_info=exc)
|
||||
raise JSONRPCParseError()
|
||||
|
||||
if isinstance(data, list):
|
||||
calls = data
|
||||
else:
|
||||
calls.append(data)
|
||||
|
||||
if len(calls) == 0:
|
||||
raise JSONRPCInvalidRequestError()
|
||||
|
||||
for call in calls:
|
||||
call_method = None
|
||||
method_handler = None
|
||||
result = None
|
||||
|
||||
try:
|
||||
try:
|
||||
assert isinstance(call, dict), JSONRPCInvalidRequestError
|
||||
assert call.get('jsonrpc') == '2.0', JSONRPCInvalidRequestError
|
||||
|
||||
call_method = call.get('method', None)
|
||||
assert call_method is not None, JSONRPCInvalidRequestError
|
||||
|
||||
method_handler = request.config_dict['RPC_METHOD_REGISTRY'].get(call_method, None)
|
||||
assert method_handler is not None, JSONRPCMethodNotFoundError
|
||||
except AssertionError as exc:
|
||||
klass = exc.args[0]
|
||||
raise klass()
|
||||
|
||||
args = []
|
||||
kwargs = {}
|
||||
|
||||
call_params = call.get('params', None)
|
||||
if call_params is not None:
|
||||
if isinstance(call_params, list):
|
||||
args = call_params
|
||||
elif isinstance(call_params, dict):
|
||||
kwargs = call_params
|
||||
else:
|
||||
raise JSONRPCInvalidParamsError()
|
||||
|
||||
args.insert(0, request)
|
||||
|
||||
try:
|
||||
result = await method_handler(*args, **kwargs)
|
||||
except TypeError as exc:
|
||||
LOGGER.error(
|
||||
f'Error handling RPC method: {call_method}!',
|
||||
exc_info=exc,
|
||||
)
|
||||
raise JSONRPCInvalidParamsError(str(exc))
|
||||
except Exception as exc:
|
||||
if isinstance(exc, BaseJSONRPCError):
|
||||
result = exc
|
||||
else:
|
||||
LOGGER.error(
|
||||
f'Error handling RPC method: {call_method}!',
|
||||
exc_info=exc,
|
||||
)
|
||||
result = JSONRPCInternalErrorError(str(exc))
|
||||
finally:
|
||||
results.append((call, result))
|
||||
except Exception as exc:
|
||||
if isinstance(exc, BaseJSONRPCError):
|
||||
results = [(None, exc)]
|
||||
else:
|
||||
raise
|
||||
|
||||
for result in results:
|
||||
call, call_result = result
|
||||
|
||||
response = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': None,
|
||||
}
|
||||
|
||||
if isinstance(call, dict) and call.get('id', None) is not None:
|
||||
response['id'] = call['id']
|
||||
|
||||
if isinstance(call_result, BaseJSONRPCError):
|
||||
response['error'] = call_result.to_json()
|
||||
else:
|
||||
response.update({
|
||||
'method': call['method'],
|
||||
'result': call_result,
|
||||
})
|
||||
|
||||
if 'error' in response or response['id'] is not None:
|
||||
responses.append(response)
|
||||
|
||||
if len(responses) == 1:
|
||||
return web.json_response(responses[0])
|
||||
|
||||
return web.json_response(responses)
|
||||
|
||||
|
||||
async def list_methods(request):
|
||||
return sorted(request.config_dict['RPC_METHOD_REGISTRY'].keys())
|
||||
|
||||
|
||||
def add_rpc_methods(app, methods):
|
||||
for name, method in methods:
|
||||
app['RPC_METHOD_REGISTRY'][name] = method
|
||||
|
||||
|
||||
def setup(app):
|
||||
app['RPC_METHOD_REGISTRY'] = {}
|
||||
app.add_rpc_methods = MethodType(add_rpc_methods, app)
|
||||
|
||||
app.add_routes([
|
||||
web.post('/backend/rpc', post_rpc),
|
||||
])
|
||||
|
||||
app.add_rpc_methods([
|
||||
('system.list_methods', list_methods),
|
||||
])
|
||||
190
packages/homehub_backend/homehub_backend/lib/services.py
Normal file
190
packages/homehub_backend/homehub_backend/lib/services.py
Normal file
@@ -0,0 +1,190 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import copy
|
||||
import logging
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.services')
|
||||
|
||||
|
||||
class BaseServiceError(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class ServiceData:
|
||||
def __init__(self, data=None, exception=None):
|
||||
super(ServiceData, self).__init__()
|
||||
self.data = data
|
||||
self.exception = exception
|
||||
|
||||
def raise_if_needed(self):
|
||||
if self.exception is not None:
|
||||
raise self.exception
|
||||
|
||||
def to_json(self):
|
||||
result = {}
|
||||
|
||||
if self.data is not None:
|
||||
result['data'] = self.data
|
||||
|
||||
if self.exception is not None:
|
||||
result['error'] = self.exception.args[0]
|
||||
|
||||
return result
|
||||
|
||||
|
||||
class BaseService:
|
||||
IS_GLOBAL = False
|
||||
INSTANCE = None
|
||||
|
||||
def __init__(self, app, instance, characteristics):
|
||||
super(BaseService, self).__init__()
|
||||
self.app = app
|
||||
self.instance = instance
|
||||
self.characteristics = characteristics
|
||||
|
||||
self.state = self.app['STATE'].get()['backend'].get(
|
||||
self.state_key(), None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def service(klass, app, instance, characteristics):
|
||||
if klass.IS_GLOBAL:
|
||||
if klass.INSTANCE is None:
|
||||
klass.INSTANCE = klass(app, None, characteristics)
|
||||
|
||||
return klass.INSTANCE
|
||||
else:
|
||||
return klass(app, instance, characteristics)
|
||||
|
||||
def state_key(self):
|
||||
return '{kind}.{instance}'.format(
|
||||
kind=self.KIND, instance=self.instance,
|
||||
)
|
||||
|
||||
async def current_data(self):
|
||||
return None
|
||||
|
||||
async def start(self):
|
||||
raise NotImplementedError('TODO')
|
||||
|
||||
async def stop(self):
|
||||
raise NotImplementedError('TODO')
|
||||
|
||||
async def shutdown(self):
|
||||
await self.stop()
|
||||
|
||||
async def notify(self):
|
||||
data = ServiceData()
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(self.current_data):
|
||||
data = await self.current_data()
|
||||
else:
|
||||
data = self.current_data()
|
||||
except Exception as exc:
|
||||
data = ServiceData(exception=exc)
|
||||
|
||||
LOGGER.debug('BaseService.notify(): {kind} {instance} {data}'.format(
|
||||
kind=self.KIND,
|
||||
instance=self.instance,
|
||||
data=str(data.to_json()),
|
||||
))
|
||||
|
||||
await self.app.notify_subscribers({
|
||||
'type': 'SERVICE_NOTIFICATION',
|
||||
'kind': self.KIND,
|
||||
'instance': self.instance if not self.IS_GLOBAL else None,
|
||||
'data': data.to_json(),
|
||||
})
|
||||
|
||||
def set_state(self, state):
|
||||
self.state = state
|
||||
|
||||
new_state = copy.deepcopy(self.app['STATE'].get())
|
||||
new_state['backend'][self.state_key()] = state
|
||||
|
||||
self.app['STATE'].save(new_state)
|
||||
|
||||
|
||||
def _get_service_key(request, kind, instance):
|
||||
klass = request.config_dict['SETTINGS'].SERVICES.get(kind, None)
|
||||
if not klass:
|
||||
raise BaseServiceError(f'Unknown service: {kind}')
|
||||
|
||||
if klass.IS_GLOBAL:
|
||||
instance = None
|
||||
|
||||
return f'{kind}.{instance}'
|
||||
|
||||
|
||||
def _lookup_service(request, kind, instance):
|
||||
service_key = _get_service_key(request, kind, instance)
|
||||
return request.config_dict['SERVICES'].get(service_key, None)
|
||||
|
||||
|
||||
async def start_service(request, kind, instance, characteristics):
|
||||
data = None
|
||||
|
||||
klass = request.config_dict['SETTINGS'].SERVICES.get(kind, None)
|
||||
if not klass:
|
||||
raise BaseServiceError(f'Unknown service: {kind}')
|
||||
else:
|
||||
service = _lookup_service(request, kind, instance)
|
||||
|
||||
if not service:
|
||||
service_key = _get_service_key(request, kind, instance)
|
||||
service = BaseService.service(
|
||||
klass, request.app, instance, characteristics,
|
||||
)
|
||||
|
||||
await service.start()
|
||||
request.config_dict['SERVICES'][service_key] = service
|
||||
|
||||
data = await service.current_data()
|
||||
data.raise_if_needed()
|
||||
|
||||
return data.data
|
||||
|
||||
|
||||
async def stop_service(request, kind, instance):
|
||||
result = 'ok'
|
||||
|
||||
service_key = _get_service_key(request, kind, instance)
|
||||
if service_key not in request.config_dict['SERVICES']:
|
||||
raise BaseServiceError(f'Unknown service: {instance} of {kind}')
|
||||
else:
|
||||
service = request.config_dict['SERVICES'].pop(service_key)
|
||||
await service.stop()
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def use_service(request, kind, instance, capability, params):
|
||||
result = None
|
||||
|
||||
service = _lookup_service(request, kind, instance)
|
||||
if not service:
|
||||
raise BaseServiceError(f'Unknown service: {instance} of {kind}')
|
||||
else:
|
||||
capability_handler = getattr(service, capability, None)
|
||||
if not capability_handler:
|
||||
raise BaseServiceError(f'Unknown capability: {kind}.{capability}')
|
||||
else:
|
||||
result = await capability_handler(*params)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def setup(app):
|
||||
app['SERVICES'] = {}
|
||||
|
||||
app.add_rpc_methods([
|
||||
('services.start', start_service),
|
||||
('services.stop', stop_service),
|
||||
('services.use', use_service),
|
||||
])
|
||||
|
||||
async def on_cleanup(app):
|
||||
for service in app['SERVICES'].values():
|
||||
await service.shutdown()
|
||||
|
||||
app.on_cleanup.append(on_cleanup)
|
||||
74
packages/homehub_backend/homehub_backend/lib/state_store.py
Normal file
74
packages/homehub_backend/homehub_backend/lib/state_store.py
Normal file
@@ -0,0 +1,74 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import concurrent.futures
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.state_store')
|
||||
|
||||
|
||||
class StateStore:
|
||||
def __init__(self, state_path):
|
||||
self.state = {'backend': {}, 'frontend': {}}
|
||||
self.state_path = state_path
|
||||
self.threadpool = None
|
||||
|
||||
def load_state(self):
|
||||
if self.state_path is not None and os.path.isfile(self.state_path):
|
||||
with codecs.open(self.state_path, 'r', 'utf-8') as state_f:
|
||||
self.state = json.load(state_f)
|
||||
|
||||
async def on_app_startup(self, app):
|
||||
LOGGER.debug('Initializing threadpool...')
|
||||
self.threadpool = concurrent.futures.ThreadPoolExecutor(max_workers=1)
|
||||
|
||||
async def on_app_cleanup(self, app):
|
||||
if self.threadpool is not None:
|
||||
LOGGER.debug('Shutting down threadpool...')
|
||||
self.threadpool.shutdown()
|
||||
|
||||
def get(self):
|
||||
return self.state
|
||||
|
||||
def do_save(self, state):
|
||||
if self.state_path is not None:
|
||||
with codecs.open(self.state_path, 'w', 'utf-8') as state_f:
|
||||
state_f.write(json.dumps(state, indent=2))
|
||||
|
||||
def save(self, state):
|
||||
self.state = state
|
||||
self.threadpool.submit(self.do_save, state)
|
||||
|
||||
|
||||
async def get_frontend_state(request):
|
||||
return request.config_dict['STATE'].get()['frontend']
|
||||
|
||||
|
||||
async def save_frontend_state(request, new_state):
|
||||
state = copy.deepcopy(request.config_dict['STATE'].get())
|
||||
state['frontend'] = new_state
|
||||
request.config_dict['STATE'].save(state)
|
||||
|
||||
await request.app.notify_subscribers({
|
||||
'type': 'STATE_NOTIFICATION',
|
||||
'data': new_state,
|
||||
})
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def setup(app):
|
||||
store = StateStore(app['SETTINGS'].STATE_PATH)
|
||||
store.load_state()
|
||||
|
||||
app.on_startup.append(store.on_app_startup)
|
||||
app.on_cleanup.append(store.on_app_cleanup)
|
||||
|
||||
app.add_rpc_methods([
|
||||
('state.get_frontend', get_frontend_state),
|
||||
('state.save_frontend', save_frontend_state),
|
||||
])
|
||||
|
||||
app['STATE'] = store
|
||||
63
packages/homehub_backend/homehub_backend/lib/websocket.py
Normal file
63
packages/homehub_backend/homehub_backend/lib/websocket.py
Normal file
@@ -0,0 +1,63 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import logging
|
||||
from types import MethodType
|
||||
|
||||
from aiohttp import WSMsgType, web
|
||||
|
||||
LOGGER = logging.getLogger('homehub.lib.websocket')
|
||||
|
||||
|
||||
async def get_websocket(request):
|
||||
response = web.WebSocketResponse()
|
||||
await response.prepare(request)
|
||||
|
||||
response['SUBSCRIBER_ID'] = datetime.datetime.now().isoformat()
|
||||
request.config_dict['SUBSCRIBERS'].append(response)
|
||||
|
||||
async for msg in response:
|
||||
if msg.type == WSMsgType.TEXT:
|
||||
LOGGER.debug('Received message: {data}'.format(data=msg.data))
|
||||
elif msg.type == WSMsgType.ERROR:
|
||||
LOGGER.error('WebSocket exception', exc_info=response.exception())
|
||||
|
||||
LOGGER.debug('WebSocket connection closed.')
|
||||
request.config_dict['SUBSCRIBERS'].remove(response)
|
||||
|
||||
return response
|
||||
|
||||
|
||||
async def notify_subscribers(app, payload):
|
||||
for subscriber in app['SUBSCRIBERS']:
|
||||
LOGGER.debug('Notifying subscriber: {subscriber_id}, closed={closed}'.format(
|
||||
subscriber_id=subscriber['SUBSCRIBER_ID'],
|
||||
closed=subscriber.closed,
|
||||
))
|
||||
|
||||
try:
|
||||
if not subscriber.closed:
|
||||
await subscriber.send_json(payload)
|
||||
except Exception as exc:
|
||||
LOGGER.error(
|
||||
'Error notifying subscriber: {subscriber_id}, closed={closed}'.format(
|
||||
subscriber_id=subscriber['SUBSCRIBER_ID'],
|
||||
closed=subscriber.closed,
|
||||
),
|
||||
exc_info=exc,
|
||||
)
|
||||
|
||||
|
||||
def setup(app):
|
||||
app['SUBSCRIBERS'] = []
|
||||
app.notify_subscribers = MethodType(notify_subscribers, app)
|
||||
|
||||
app.add_routes([
|
||||
web.get('/backend/websocket', get_websocket),
|
||||
])
|
||||
|
||||
async def on_shutdown(app):
|
||||
for subscriber in app['SUBSCRIBERS']:
|
||||
if not subscriber.closed:
|
||||
await subscriber.close()
|
||||
|
||||
app.on_shutdown.append(on_shutdown)
|
||||
@@ -0,0 +1,9 @@
|
||||
from homehub_backend.defs import services as services_defs
|
||||
|
||||
from .uptime import UptimeService
|
||||
from .weather import WeatherService
|
||||
|
||||
SERVICES = {
|
||||
services_defs.kServiceUptime: UptimeService,
|
||||
services_defs.kServiceWeather: WeatherService,
|
||||
}
|
||||
52
packages/homehub_backend/homehub_backend/services/uptime.py
Normal file
52
packages/homehub_backend/homehub_backend/services/uptime.py
Normal file
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import datetime
|
||||
import logging
|
||||
|
||||
import aiojobs.aiohttp
|
||||
|
||||
from homehub_backend.defs import kServiceUptime
|
||||
from homehub_backend.lib.services import BaseService, ServiceData
|
||||
|
||||
LOGGER = logging.getLogger('homehub.services.uptime')
|
||||
|
||||
|
||||
class UptimeService(BaseService):
|
||||
KIND = kServiceUptime
|
||||
IS_GLOBAL = True
|
||||
|
||||
def __init__(self, app, instance, characteristics):
|
||||
super(UptimeService, self).__init__(app, instance, characteristics)
|
||||
self.uptime = 0
|
||||
self.job = None
|
||||
|
||||
def update_uptime(self):
|
||||
delta = datetime.datetime.now() - self.app['STARTUP_TS']
|
||||
self.uptime = delta.seconds
|
||||
|
||||
async def current_data(self):
|
||||
return ServiceData(data=self.uptime)
|
||||
|
||||
async def worker(self):
|
||||
while True:
|
||||
await asyncio.sleep(60)
|
||||
self.update_uptime()
|
||||
await self.notify()
|
||||
|
||||
if self.job.closed:
|
||||
break
|
||||
|
||||
async def start(self):
|
||||
LOGGER.debug('UptimeService.start()')
|
||||
if not self.job:
|
||||
self.update_uptime()
|
||||
scheduler = aiojobs.aiohttp.get_scheduler_from_app(self.app)
|
||||
self.job = await scheduler.spawn(self.worker())
|
||||
|
||||
async def stop(self):
|
||||
LOGGER.debug('UptimeService.stop()')
|
||||
|
||||
async def shutdown(self):
|
||||
LOGGER.debug('UptimeService.shutdown()')
|
||||
if self.job is not None and not self.job.closed:
|
||||
await self.job.close()
|
||||
102
packages/homehub_backend/homehub_backend/services/weather.py
Normal file
102
packages/homehub_backend/homehub_backend/services/weather.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
import aiohttp
|
||||
import aiojobs.aiohttp
|
||||
|
||||
from homehub_backend.defs import kServiceWeather
|
||||
from homehub_backend.lib.services import BaseService, ServiceData
|
||||
|
||||
LOGGER = logging.getLogger('homehub.services.weather')
|
||||
|
||||
|
||||
class WeatherService(BaseService):
|
||||
KIND = kServiceWeather
|
||||
API_KEY_SETTINGS_KEY = 'WEATHER_SERVICE_API_KEY'
|
||||
API_URL = 'https://api.openweathermap.org/data/2.5/weather'
|
||||
|
||||
def __init__(self, app, instance, characteristics):
|
||||
super(WeatherService, self).__init__(app, instance, characteristics)
|
||||
self._current_data = None
|
||||
|
||||
self.api_key = None
|
||||
self.job = None
|
||||
self.session = None
|
||||
|
||||
async def update_weather(self):
|
||||
LOGGER.debug('WeatherService.update_weather(): {city}'.format(
|
||||
city=self.characteristics['city'],
|
||||
))
|
||||
|
||||
params = {
|
||||
'APPID': self.api_key,
|
||||
'q': self.characteristics['city'],
|
||||
'units': self.characteristics['units'],
|
||||
}
|
||||
|
||||
result = ServiceData()
|
||||
|
||||
response = None
|
||||
try:
|
||||
response = await self.session.get(self.API_URL, params=params)
|
||||
if response.status != 200:
|
||||
raise RuntimeError('HTTP {status} {reason}'.format(
|
||||
status=response.status,
|
||||
reason=response.reason,
|
||||
))
|
||||
else:
|
||||
result.data = await response.json()
|
||||
except Exception as exc:
|
||||
LOGGER.error('Unhandled exception!', exc_info=exc)
|
||||
result.exception = exc
|
||||
finally:
|
||||
if response:
|
||||
response.release()
|
||||
|
||||
self._current_data = result
|
||||
|
||||
async def current_data(self):
|
||||
if self._current_data is None:
|
||||
return ServiceData()
|
||||
|
||||
return self._current_data
|
||||
|
||||
async def worker(self):
|
||||
while True:
|
||||
await asyncio.sleep(60 * 10)
|
||||
await self.update_weather()
|
||||
await self.notify()
|
||||
|
||||
if self.job.closed:
|
||||
break
|
||||
|
||||
async def start(self):
|
||||
LOGGER.debug('WeatherService.start()')
|
||||
|
||||
self.api_key = getattr(
|
||||
self.app['SETTINGS'], self.API_KEY_SETTINGS_KEY, None,
|
||||
)
|
||||
assert self.api_key is not None, 'API key is missing'
|
||||
|
||||
assert self.characteristics['city'] is not None, 'City is missing'
|
||||
assert self.characteristics['city'] != '', 'City is empty'
|
||||
|
||||
if not self.session:
|
||||
self.session = aiohttp.ClientSession()
|
||||
|
||||
if not self._current_data:
|
||||
await self.update_weather()
|
||||
|
||||
if not self.job:
|
||||
scheduler = aiojobs.aiohttp.get_scheduler_from_app(self.app)
|
||||
self.job = await scheduler.spawn(self.worker())
|
||||
|
||||
async def stop(self):
|
||||
LOGGER.debug('WeatherService.stop()')
|
||||
|
||||
if self.session and not self.session.closed:
|
||||
await self.session.close()
|
||||
|
||||
if self.job is not None and not self.job.closed:
|
||||
await self.job.close()
|
||||
10
packages/homehub_backend/homehub_backend/settings.py
Normal file
10
packages/homehub_backend/homehub_backend/settings.py
Normal file
@@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from homehub_backend.services import SERVICES # noqa: F401
|
||||
|
||||
DEBUG = False
|
||||
TESTING = False
|
||||
STATE_PATH = os.path.join(os.getcwd(), 'state.json')
|
||||
|
||||
WEATHER_SERVICE_API_KEY = None
|
||||
1
packages/homehub_backend/homehub_backend/testing/.gitignore
vendored
Normal file
1
packages/homehub_backend/homehub_backend/testing/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
state.json
|
||||
21
packages/homehub_backend/homehub_backend/testing/app.py
Normal file
21
packages/homehub_backend/homehub_backend/testing/app.py
Normal file
@@ -0,0 +1,21 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.app import create_app
|
||||
|
||||
|
||||
async def greet(request, who, how='Hello,'):
|
||||
return f'{how} {who}!'
|
||||
|
||||
|
||||
async def fial(request):
|
||||
raise RuntimeError('FIAL')
|
||||
|
||||
|
||||
def create_testing_app(loop=None):
|
||||
app = create_app(loop=None)
|
||||
|
||||
app.add_rpc_methods([
|
||||
('testing.greet', greet),
|
||||
('testing.fial', fial),
|
||||
])
|
||||
|
||||
return app
|
||||
45
packages/homehub_backend/homehub_backend/testing/services.py
Normal file
45
packages/homehub_backend/homehub_backend/testing/services.py
Normal file
@@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from homehub_backend.lib import services
|
||||
|
||||
|
||||
class FakeService(services.BaseService):
|
||||
KIND = 'testing.FakeService'
|
||||
|
||||
@classmethod
|
||||
def make_data(cls):
|
||||
return services.ServiceData(data='spam')
|
||||
|
||||
async def current_data(self):
|
||||
return self.make_data()
|
||||
|
||||
start = mock.AsyncMock()
|
||||
|
||||
stop = mock.AsyncMock()
|
||||
|
||||
async def capability(self, param):
|
||||
return param
|
||||
|
||||
|
||||
class FakeGlobalService(services.BaseService):
|
||||
IS_GLOBAL = True
|
||||
KIND = 'testing.FakeGlobalService'
|
||||
|
||||
@classmethod
|
||||
def reset(cls):
|
||||
if cls.INSTANCE:
|
||||
cls.INSTANCE.start.reset_mock()
|
||||
cls.INSTANCE.stop.reset_mock()
|
||||
cls.INSTANCE = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(FakeGlobalService, self).__init__(*args, **kwargs)
|
||||
self.data = services.ServiceData(data='spam')
|
||||
|
||||
async def current_data(self):
|
||||
return self.data
|
||||
|
||||
start = mock.AsyncMock()
|
||||
|
||||
stop = mock.AsyncMock()
|
||||
14
packages/homehub_backend/homehub_backend/testing/settings.py
Normal file
14
packages/homehub_backend/homehub_backend/testing/settings.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
|
||||
from homehub_backend.testing import services
|
||||
|
||||
DEBUG = False
|
||||
TESTING = True
|
||||
STATE_PATH = os.path.join(os.path.dirname(__file__), 'state.json')
|
||||
|
||||
WEATHER_SERVICE_API_KEY = 'thisisntright'
|
||||
|
||||
SERVICES = {}
|
||||
SERVICES[services.FakeService.KIND] = services.FakeService
|
||||
SERVICES[services.FakeGlobalService.KIND] = services.FakeGlobalService
|
||||
14
packages/homehub_backend/homehub_backend/tests/conftest.py
Normal file
14
packages/homehub_backend/homehub_backend/tests/conftest.py
Normal file
@@ -0,0 +1,14 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import pytest
|
||||
|
||||
from homehub_backend.testing.app import create_testing_app
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def homehub_app(loop):
|
||||
return create_testing_app(loop=loop)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def homehub_client(loop, homehub_app, aiohttp_client):
|
||||
return loop.run_until_complete(aiohttp_client(homehub_app()))
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.lib import rpc
|
||||
|
||||
|
||||
class Test_BaseJSONRPCError:
|
||||
def test_to_json(self):
|
||||
exception = rpc.BaseJSONRPCError(data='spam')
|
||||
exception.ERROR_CODE = -32604
|
||||
exception.ERROR_MESSAGE = 'Test error'
|
||||
|
||||
result = exception.to_json()
|
||||
assert result['code'] == exception.ERROR_CODE
|
||||
assert result['message'] == exception.ERROR_MESSAGE
|
||||
assert result['data'] == exception.data
|
||||
|
||||
def test_to_json_without_data(self):
|
||||
exception = rpc.BaseJSONRPCError()
|
||||
exception.ERROR_CODE = -32604
|
||||
exception.ERROR_MESSAGE = 'Test error'
|
||||
|
||||
result = exception.to_json()
|
||||
assert result['code'] == exception.ERROR_CODE
|
||||
assert result['message'] == exception.ERROR_MESSAGE
|
||||
assert 'data' not in result
|
||||
@@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.lib import rpc
|
||||
|
||||
|
||||
class Test_PostRPC:
|
||||
def _valid_call(self, **kwargs):
|
||||
result = {
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'testing.greet',
|
||||
}
|
||||
result.update(kwargs)
|
||||
|
||||
return result
|
||||
|
||||
async def test_parse_error(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', data=b'spam')
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCParseError.ERROR_CODE
|
||||
|
||||
async def test_empty_call(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_empty_batch(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json=[])
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_not_dict(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json='"spam"')
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_without_jsonrpc(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call.pop('jsonrpc')
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_call_without_method(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call.pop('method')
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidRequestError.ERROR_CODE
|
||||
|
||||
async def test_method_not_found(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['method'] = 'testing.idontexist'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCMethodNotFoundError.ERROR_CODE
|
||||
|
||||
async def test_invalid_params(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = 'spam'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidParamsError.ERROR_CODE
|
||||
assert 'data' not in data['error']
|
||||
|
||||
async def test_call_with_method_params_error(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInvalidParamsError.ERROR_CODE
|
||||
assert data['error']['data'] == (
|
||||
"greet() missing 1 required positional argument: 'who'"
|
||||
)
|
||||
|
||||
async def test_method_exception(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['method'] = 'testing.fial'
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['code'] == rpc.JSONRPCInternalErrorError.ERROR_CODE
|
||||
assert data['error']['data'] == 'FIAL'
|
||||
|
||||
async def test_call_with_args(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = ['aiohttp-jsonrpc']
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['result'] == 'Hello, aiohttp-jsonrpc!'
|
||||
|
||||
async def test_call_with_kwargs(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = {
|
||||
'who': 'aiohttp-jsonrpc',
|
||||
'how': 'Hi,',
|
||||
}
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['result'] == 'Hi, aiohttp-jsonrpc!'
|
||||
|
||||
async def test_result(self, homehub_client):
|
||||
call = self._valid_call()
|
||||
call['params'] = ['aiohttp-jsonrpc']
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=call)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'error' not in data
|
||||
assert data['jsonrpc'] == call['jsonrpc']
|
||||
assert data['id'] == call['id']
|
||||
assert data['method'] == call['method']
|
||||
|
||||
async def test_batch(self, homehub_client):
|
||||
call_without_id = self._valid_call(params=['World'])
|
||||
call_without_id.pop('id')
|
||||
|
||||
batch = [
|
||||
self._valid_call(params=['aiohttp-jsonrpc']),
|
||||
self._valid_call(id='testing2', method='testing.fial', params=[]),
|
||||
call_without_id,
|
||||
]
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json=batch)
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert isinstance(data, list) is True
|
||||
assert len(data) == 2
|
||||
|
||||
first_result, second_result = data
|
||||
|
||||
assert first_result['id'] == batch[0]['id']
|
||||
assert 'error' not in first_result
|
||||
assert first_result['result'] == 'Hello, aiohttp-jsonrpc!'
|
||||
|
||||
assert second_result['id'] == batch[1]['id']
|
||||
assert 'result' not in second_result
|
||||
assert second_result['error']['code'] == rpc.JSONRPCInternalErrorError.ERROR_CODE
|
||||
assert second_result['error']['data'] == 'FIAL'
|
||||
@@ -0,0 +1,204 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from homehub_backend.app import app
|
||||
from homehub_backend.lib import services
|
||||
from homehub_backend.testing.services import FakeService, FakeGlobalService
|
||||
|
||||
|
||||
class Test_BaseService:
|
||||
def tearDown(self):
|
||||
app['SUBSCRIBERS'] = []
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
def test_init(self, mock_app_state_get):
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
assert service.app == app
|
||||
assert service.instance == instance
|
||||
assert service.characteristics == characteristics
|
||||
assert service.state == 'fake_state'
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
def test_service_not_global(self, mock_app_state_get):
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = services.BaseService.service(
|
||||
FakeService, app, instance, characteristics,
|
||||
)
|
||||
assert FakeService.INSTANCE is None
|
||||
assert service.app == app
|
||||
assert service.instance == instance
|
||||
assert service.characteristics == characteristics
|
||||
assert service.state == 'fake_state'
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
def test_service_global(self, mock_app_state_get):
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeGlobalService.None': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = services.BaseService.service(
|
||||
FakeGlobalService, app, instance, characteristics,
|
||||
)
|
||||
assert FakeGlobalService.INSTANCE == service
|
||||
assert service.app == app
|
||||
assert service.instance is None
|
||||
assert service.characteristics == characteristics
|
||||
assert service.state == 'fake_state'
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
def test_state_key(self, mock_app_state_get):
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
result = service.state_key()
|
||||
assert result == 'testing.FakeService.fake_instance'
|
||||
|
||||
async def test_current_data(self):
|
||||
with mock.patch.object(app['STATE'], 'get') as mock_app_state_get:
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
result = await service.current_data()
|
||||
assert isinstance(result, services.ServiceData)
|
||||
assert result.data == 'spam'
|
||||
|
||||
async def test_shutdown(self):
|
||||
with mock.patch.object(app['STATE'], 'get') as mock_app_state_get:
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
with mock.patch.object(service, 'stop'):
|
||||
await service.shutdown()
|
||||
assert service.stop.called is True
|
||||
|
||||
async def test_notify(self):
|
||||
with mock.patch.object(app['STATE'], 'get') as mock_app_state_get:
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
with mock.patch.object(app, 'notify_subscribers'):
|
||||
await service.notify()
|
||||
|
||||
app.notify_subscribers.assert_called_with({
|
||||
'type': 'SERVICE_NOTIFICATION',
|
||||
'kind': FakeService.KIND,
|
||||
'instance': 'fake_instance',
|
||||
'data': service.make_data().to_json(),
|
||||
})
|
||||
|
||||
async def test_notify_global(self):
|
||||
with mock.patch.object(app['STATE'], 'get') as mock_app_state_get:
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeGlobalService(app, instance, characteristics)
|
||||
|
||||
with mock.patch.object(app, 'notify_subscribers'):
|
||||
await service.notify()
|
||||
|
||||
app.notify_subscribers.assert_called_with({
|
||||
'type': 'SERVICE_NOTIFICATION',
|
||||
'kind': FakeGlobalService.KIND,
|
||||
'instance': None,
|
||||
'data': service.data.to_json(),
|
||||
})
|
||||
|
||||
async def test_notify_exception(self):
|
||||
with mock.patch.object(app['STATE'], 'get') as mock_app_state_get:
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
with mock.patch.object(service, 'current_data') as mock_current_data:
|
||||
mock_current_data.side_effect = RuntimeError('TODO')
|
||||
|
||||
with mock.patch.object(app, 'notify_subscribers'):
|
||||
await service.notify()
|
||||
|
||||
app.notify_subscribers.assert_called_with({
|
||||
'type': 'SERVICE_NOTIFICATION',
|
||||
'kind': FakeService.KIND,
|
||||
'instance': 'fake_instance',
|
||||
'data': {
|
||||
'error': 'TODO',
|
||||
},
|
||||
})
|
||||
|
||||
@mock.patch.object(app['STATE'], 'get')
|
||||
@mock.patch.object(app['STATE'], 'save')
|
||||
def test_set_state(self, mock_app_state_save, mock_app_state_get):
|
||||
mock_app_state_get.return_value = {
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'fake_state',
|
||||
},
|
||||
}
|
||||
instance = 'fake_instance'
|
||||
characteristics = {'spam': True}
|
||||
|
||||
service = FakeService(app, instance, characteristics)
|
||||
|
||||
service.set_state('spam')
|
||||
assert service.state == 'spam'
|
||||
mock_app_state_save.assert_called_with({
|
||||
'backend': {
|
||||
'testing.FakeService.fake_instance': 'spam',
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.lib import services
|
||||
|
||||
|
||||
class Test_ServiceData:
|
||||
def test_init(self):
|
||||
exc = RuntimeError('TODO')
|
||||
|
||||
service_data = services.ServiceData(data='spam', exception=exc)
|
||||
assert service_data.data == 'spam'
|
||||
assert service_data.exception == exc
|
||||
|
||||
def test_raise_if_needed_raise(self):
|
||||
exc = RuntimeError('TODO')
|
||||
|
||||
service_data = services.ServiceData(data='spam', exception=exc)
|
||||
raised_exception = None
|
||||
try:
|
||||
service_data.raise_if_needed()
|
||||
except Exception as _exc:
|
||||
raised_exception = _exc
|
||||
|
||||
assert raised_exception == exc, 'Did not raise?'
|
||||
|
||||
def test_raise_if_needed_dont_raise(self):
|
||||
service_data = services.ServiceData(data='spam', exception=None)
|
||||
raised_exception = None
|
||||
try:
|
||||
service_data.raise_if_needed()
|
||||
except Exception as _exc:
|
||||
raised_exception = _exc
|
||||
|
||||
assert raised_exception is None, f'Raised {raised_exception}'
|
||||
|
||||
def test_to_json(self):
|
||||
exc = RuntimeError('TODO')
|
||||
service_data = services.ServiceData(data='spam', exception=exc)
|
||||
|
||||
result = service_data.to_json()
|
||||
assert result['data'] == service_data.data
|
||||
assert result['error'] == exc.args[0]
|
||||
|
||||
def test_to_json_no_data(self):
|
||||
exc = RuntimeError('TODO')
|
||||
service_data = services.ServiceData(data=None, exception=exc)
|
||||
|
||||
result = service_data.to_json()
|
||||
assert 'data' not in result
|
||||
assert result['error'] == exc.args[0]
|
||||
|
||||
def test_to_json_no_exception(self):
|
||||
service_data = services.ServiceData(data='spam', exception=None)
|
||||
|
||||
result = service_data.to_json()
|
||||
assert result['data'] == service_data.data
|
||||
assert 'error' not in result
|
||||
@@ -0,0 +1,109 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from homehub_backend.lib import services
|
||||
from homehub_backend.testing import services as testing_services
|
||||
|
||||
|
||||
class Test_StartService:
|
||||
def teardown_method(self):
|
||||
testing_services.FakeGlobalService.reset()
|
||||
|
||||
async def test_unknown_service(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': ['IDontExist', 'fake_instance', {'spam': True}],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == 'Unknown service: IDontExist'
|
||||
|
||||
async def test_not_global(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': ['testing.FakeService', 'fake_instance', {'spam': True}],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] == 'spam'
|
||||
|
||||
service = homehub_client.server.app['SERVICES'].get(
|
||||
'testing.FakeService.fake_instance', None,
|
||||
)
|
||||
assert service is not None
|
||||
assert service.characteristics == {'spam': True}
|
||||
assert service.start.called is True
|
||||
|
||||
async def test_global(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': [
|
||||
'testing.FakeGlobalService', 'fake_instance', {'spam': True},
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] == 'spam'
|
||||
|
||||
service = homehub_client.server.app['SERVICES'].get(
|
||||
'testing.FakeGlobalService.None', None,
|
||||
)
|
||||
assert service is not None
|
||||
assert service.characteristics == {'spam': True}
|
||||
assert service.start.call_count == 1
|
||||
|
||||
async def test_global_ensure_one_instance(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': [
|
||||
'testing.FakeGlobalService', 'fake_instance', {'spam': True},
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
service = homehub_client.server.app['SERVICES'].get(
|
||||
'testing.FakeGlobalService.None', None,
|
||||
)
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': [
|
||||
'testing.FakeGlobalService', 'fake_instance', {'spam': True},
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
assert testing_services.FakeGlobalService.INSTANCE == service
|
||||
assert service.start.call_count == 1
|
||||
|
||||
@mock.patch.object(testing_services.FakeService, 'make_data')
|
||||
async def test_data_error(self, mock_service_make_data, homehub_client):
|
||||
mock_service_make_data.return_value = services.ServiceData(
|
||||
exception=RuntimeError('TODO'),
|
||||
)
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.start',
|
||||
'params': ['testing.FakeService', 'fake_instance', {'spam': True}],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == 'TODO'
|
||||
@@ -0,0 +1,56 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.testing import services as testing_services
|
||||
|
||||
|
||||
class Test_StopService:
|
||||
async def test_unknown_service_instance(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.stop',
|
||||
'params': ['testing.FakeService', 'idontexist'],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == (
|
||||
'Unknown service: idontexist of testing.FakeService'
|
||||
)
|
||||
|
||||
async def test_unknown_service_kind(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.stop',
|
||||
'params': ['IDontExist', 'fake_instance'],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == 'Unknown service: IDontExist'
|
||||
|
||||
async def test_ok(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.stop',
|
||||
'params': ['testing.FakeService', 'fake_instance'],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] == 'ok'
|
||||
assert 'error' not in data
|
||||
|
||||
assert service.stop.called is True
|
||||
|
||||
assert len(homehub_client.server.app['SERVICES']) == 0
|
||||
@@ -0,0 +1,86 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from homehub_backend.testing import services as testing_services
|
||||
|
||||
|
||||
class Test_UseService:
|
||||
async def test_unknown_service_instance(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.use',
|
||||
'params': [
|
||||
'testing.FakeService', 'idontexist', 'capability',
|
||||
['It works!'],
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == (
|
||||
'Unknown service: idontexist of testing.FakeService'
|
||||
)
|
||||
|
||||
async def test_unknown_service_kind(self, homehub_client):
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.use',
|
||||
'params': [
|
||||
'IDontExist', 'fake_instance', 'capability',
|
||||
['It works!'],
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == 'Unknown service: IDontExist'
|
||||
|
||||
async def test_unknown_capability(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.use',
|
||||
'params': [
|
||||
'testing.FakeService', 'fake_instance', 'idontexist',
|
||||
['It works!'],
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert 'result' not in data
|
||||
assert data['error']['data'] == (
|
||||
'Unknown capability: testing.FakeService.idontexist'
|
||||
)
|
||||
|
||||
async def test_ok(self, homehub_client):
|
||||
service = testing_services.FakeService(
|
||||
homehub_client.server.app, 'fake_instance', {},
|
||||
)
|
||||
homehub_client.server.app['SERVICES'].update({
|
||||
'testing.FakeService.fake_instance': service,
|
||||
})
|
||||
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'services.use',
|
||||
'params': [
|
||||
'testing.FakeService', 'fake_instance', 'capability',
|
||||
['It works!'],
|
||||
],
|
||||
})
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] == 'It works!'
|
||||
assert 'error' not in data
|
||||
@@ -0,0 +1,135 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import codecs
|
||||
import concurrent.futures
|
||||
import json
|
||||
import os
|
||||
import tempfile
|
||||
from unittest import mock
|
||||
|
||||
from homehub_backend.lib import state_store
|
||||
|
||||
|
||||
class Test_StateStore:
|
||||
def setup_method(self):
|
||||
fh, self.state_path = tempfile.mkstemp()
|
||||
os.close(fh)
|
||||
|
||||
def teardown_method(self):
|
||||
os.remove(self.state_path)
|
||||
|
||||
def test_init(self):
|
||||
store = state_store.StateStore(self.state_path)
|
||||
assert store.state == {'backend': {}, 'frontend': {}}
|
||||
assert store.state_path == self.state_path
|
||||
assert store.threadpool is None
|
||||
|
||||
def test_load_state(self):
|
||||
# Given
|
||||
saved_state = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': {
|
||||
'eggs': True,
|
||||
},
|
||||
}
|
||||
|
||||
with codecs.open(self.state_path, 'w', 'utf-8') as state_f:
|
||||
state_f.write(json.dumps(saved_state))
|
||||
|
||||
store = state_store.StateStore(self.state_path)
|
||||
|
||||
# When
|
||||
store.load_state()
|
||||
|
||||
# Then
|
||||
assert store.state == saved_state
|
||||
|
||||
async def test_on_app_startup(self, homehub_app):
|
||||
# Given
|
||||
fake_threadpool_executor = mock.Mock(
|
||||
spec=concurrent.futures.ThreadPoolExecutor,
|
||||
)
|
||||
|
||||
with mock.patch.object(concurrent.futures, 'ThreadPoolExecutor') as mock_threadpool_executor:
|
||||
mock_threadpool_executor.return_value = fake_threadpool_executor
|
||||
|
||||
store = state_store.StateStore(self.state_path)
|
||||
|
||||
# When
|
||||
await store.on_app_startup(homehub_app)
|
||||
|
||||
# Then
|
||||
assert store.threadpool == fake_threadpool_executor
|
||||
mock_threadpool_executor.assert_called_with(max_workers=1)
|
||||
|
||||
async def test_on_app_cleanup(self, homehub_app):
|
||||
# Given
|
||||
store = state_store.StateStore(self.state_path)
|
||||
store.threadpool = mock.Mock(
|
||||
spec=concurrent.futures.ThreadPoolExecutor,
|
||||
)
|
||||
|
||||
# When
|
||||
await store.on_app_cleanup(homehub_app)
|
||||
|
||||
# Then
|
||||
assert store.threadpool.shutdown.called is True
|
||||
|
||||
def test_get(self):
|
||||
# Given
|
||||
store = state_store.StateStore(self.state_path)
|
||||
|
||||
# When
|
||||
result = store.get()
|
||||
|
||||
# Then
|
||||
assert result is store.state
|
||||
|
||||
def test_do_save(self):
|
||||
# Given
|
||||
state_to_save = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': {
|
||||
'eggs': True,
|
||||
},
|
||||
}
|
||||
|
||||
store = state_store.StateStore(self.state_path)
|
||||
|
||||
# When
|
||||
store.do_save(state_to_save)
|
||||
|
||||
# Then
|
||||
saved_state = None
|
||||
with codecs.open(self.state_path, 'r', 'utf-8') as state_f:
|
||||
saved_state = json.load(state_f)
|
||||
|
||||
assert saved_state == state_to_save
|
||||
|
||||
def test_save(self):
|
||||
# Given
|
||||
state_to_save = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': {
|
||||
'eggs': True,
|
||||
},
|
||||
}
|
||||
|
||||
store = state_store.StateStore(self.state_path)
|
||||
store.threadpool = mock.Mock(
|
||||
spec=concurrent.futures.ThreadPoolExecutor,
|
||||
)
|
||||
|
||||
# When
|
||||
store.save(state_to_save)
|
||||
|
||||
# Then
|
||||
assert store.state == state_to_save
|
||||
store.threadpool.submit.assert_called_with(
|
||||
store.do_save, state_to_save,
|
||||
)
|
||||
@@ -0,0 +1,32 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
|
||||
async def test_get_frontend_state(homehub_app, homehub_client):
|
||||
# Given
|
||||
fake_state = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': {
|
||||
'eggs': True,
|
||||
},
|
||||
}
|
||||
|
||||
with mock.patch.object(homehub_app['STATE'], 'get') as mock_state_get:
|
||||
mock_state_get.return_value = fake_state
|
||||
|
||||
# When
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'state.get_frontend',
|
||||
'params': [],
|
||||
})
|
||||
|
||||
# Then
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] == fake_state['frontend']
|
||||
assert 'error' not in data
|
||||
@@ -0,0 +1,49 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
|
||||
async def test_save_frontend_state(homehub_app, homehub_client):
|
||||
# Given
|
||||
fake_state = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': {
|
||||
'eggs': True,
|
||||
},
|
||||
}
|
||||
|
||||
payload = {'new': True}
|
||||
|
||||
state_to_save = {
|
||||
'backend': {
|
||||
'spam': True,
|
||||
},
|
||||
'frontend': payload,
|
||||
}
|
||||
|
||||
with mock.patch.object(homehub_app['STATE'], 'get') as mock_state_get:
|
||||
with mock.patch.object(homehub_app['STATE'], 'save'):
|
||||
with mock.patch.object(homehub_app, 'notify_subscribers'):
|
||||
mock_state_get.return_value = fake_state
|
||||
|
||||
# When
|
||||
response = await homehub_client.post('/backend/rpc', json={
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'testing',
|
||||
'method': 'state.save_frontend',
|
||||
'params': [payload],
|
||||
})
|
||||
|
||||
# Then
|
||||
assert response.status == 200
|
||||
|
||||
data = await response.json()
|
||||
assert data['result'] is True
|
||||
assert 'error' not in data
|
||||
|
||||
homehub_app['STATE'].save.assert_called_with(state_to_save)
|
||||
homehub_app.notify_subscribers.assert_called_with({
|
||||
'type': 'STATE_NOTIFICATION',
|
||||
'data': payload,
|
||||
})
|
||||
@@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
from aiohttp.web import WebSocketResponse
|
||||
|
||||
from homehub_backend.app import app
|
||||
from homehub_backend.lib import websocket
|
||||
|
||||
|
||||
async def test_notify_subscribers():
|
||||
# Given
|
||||
error_subscriber = WebSocketResponse()
|
||||
error_subscriber['SUBSCRIBER_ID'] = 'test_notify_subscribers.error_subscriber'
|
||||
app['SUBSCRIBERS'].append(error_subscriber)
|
||||
|
||||
ok_subscriber = WebSocketResponse()
|
||||
ok_subscriber['SUBSCRIBER_ID'] = 'test_notify_subscribers.ok_subscriber'
|
||||
app['SUBSCRIBERS'].append(ok_subscriber)
|
||||
|
||||
closed_subscriber = WebSocketResponse()
|
||||
closed_subscriber['SUBSCRIBER_ID'] = 'test_notify_subscribers.closed_subscriber'
|
||||
closed_subscriber._closed = True
|
||||
app['SUBSCRIBERS'].append(closed_subscriber)
|
||||
|
||||
payload = {'spam': True}
|
||||
|
||||
with mock.patch.object(error_subscriber, 'send_json') as error_subscriber_send_json:
|
||||
with mock.patch.object(ok_subscriber, 'send_json'):
|
||||
with mock.patch.object(closed_subscriber, 'send_json'):
|
||||
error_subscriber_send_json.side_effect = RuntimeError('FIAL')
|
||||
|
||||
# When
|
||||
await websocket.notify_subscribers(app, payload)
|
||||
|
||||
# Then
|
||||
error_subscriber.send_json.assert_called_with(payload)
|
||||
ok_subscriber.send_json.assert_called_with(payload)
|
||||
assert not closed_subscriber.send_json.called
|
||||
|
||||
# After
|
||||
app['SUBSCRIBERS'].remove(error_subscriber)
|
||||
app['SUBSCRIBERS'].remove(ok_subscriber)
|
||||
app['SUBSCRIBERS'].remove(closed_subscriber)
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
from unittest import mock
|
||||
|
||||
import aiojobs
|
||||
|
||||
from homehub_backend.services import uptime
|
||||
|
||||
|
||||
class Test_UptimeService:
|
||||
def test_init(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
assert service.uptime == 0
|
||||
assert service.job is None
|
||||
|
||||
def test_update_uptime(self, homehub_app):
|
||||
homehub_app['STARTUP_TS'] = datetime.datetime(1987, 10, 3, 8, 0, 0)
|
||||
fake_now = homehub_app['STARTUP_TS'] + datetime.timedelta(minutes=15)
|
||||
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
|
||||
with mock.patch('datetime.datetime') as mock_datetime:
|
||||
mock_datetime.now.return_value = fake_now
|
||||
|
||||
service.update_uptime()
|
||||
assert service.uptime == 900
|
||||
|
||||
async def test_current_data(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.uptime = 42
|
||||
|
||||
result = await service.current_data()
|
||||
assert result.data == 42
|
||||
|
||||
async def test_worker(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = True
|
||||
service.update_uptime = mock.Mock()
|
||||
service.notify = mock.AsyncMock()
|
||||
|
||||
with mock.patch('asyncio.sleep') as mock_asyncio_sleep:
|
||||
await service.worker()
|
||||
|
||||
mock_asyncio_sleep.assert_called_with(60)
|
||||
assert service.update_uptime.called is True
|
||||
assert service.notify.called is True
|
||||
|
||||
async def test_start(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.update_uptime = mock.Mock()
|
||||
|
||||
fake_worker = mock.AsyncMock()
|
||||
service.worker = mock.Mock()
|
||||
service.worker.return_value = fake_worker
|
||||
|
||||
with mock.patch('aiojobs.aiohttp.get_scheduler_from_app') as mock_get_scheduler:
|
||||
fake_scheduler = mock.Mock(spec=aiojobs._scheduler.Scheduler)
|
||||
fake_scheduler.spawn.return_value = mock.Mock(
|
||||
spec=aiojobs._job.Job,
|
||||
)
|
||||
mock_get_scheduler.return_value = fake_scheduler
|
||||
|
||||
await service.start()
|
||||
assert service.update_uptime.called is True
|
||||
mock_get_scheduler.assert_called_with(homehub_app)
|
||||
fake_scheduler.spawn.assert_called_with(fake_worker)
|
||||
|
||||
async def test_start_already_started(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.update_uptime = mock.Mock()
|
||||
|
||||
fake_worker = mock.AsyncMock()
|
||||
service.worker = mock.Mock()
|
||||
service.worker.return_value = fake_worker
|
||||
|
||||
with mock.patch('aiojobs.aiohttp.get_scheduler_from_app') as mock_get_scheduler:
|
||||
fake_scheduler = mock.Mock(spec=aiojobs._scheduler.Scheduler)
|
||||
fake_scheduler.spawn.return_value = mock.Mock(
|
||||
spec=aiojobs._job.Job,
|
||||
)
|
||||
|
||||
mock_get_scheduler.return_value = fake_scheduler
|
||||
|
||||
await service.start()
|
||||
assert not service.update_uptime.called
|
||||
assert not mock_get_scheduler.called
|
||||
assert not fake_scheduler.spawn.called
|
||||
|
||||
async def test_shutdown(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = False
|
||||
|
||||
await service.shutdown()
|
||||
assert service.job.close.called is True
|
||||
|
||||
async def test_shutdown_job_already_closed(self, homehub_app):
|
||||
service = uptime.UptimeService(homehub_app, 'testing', {})
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = True
|
||||
|
||||
await service.shutdown()
|
||||
assert not service.job.close.called
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from unittest import mock
|
||||
|
||||
import aiohttp
|
||||
import aiojobs
|
||||
|
||||
from homehub_backend.lib.services import ServiceData
|
||||
from homehub_backend.services import weather
|
||||
|
||||
|
||||
class Test_WeatherService:
|
||||
def _valid_characteristics(self):
|
||||
return {
|
||||
'city': 'Wroclaw,PL',
|
||||
'units': 'metric',
|
||||
}
|
||||
|
||||
def test_init(self, homehub_app):
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', self._valid_characteristics(),
|
||||
)
|
||||
assert service._current_data is None
|
||||
assert service.api_key is None
|
||||
assert service.job is None
|
||||
assert service.session is None
|
||||
|
||||
async def test_update_weather(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.api_key = 'api_key'
|
||||
service.session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
|
||||
fake_data = {'spam': True}
|
||||
fake_response = mock.Mock(spec=aiohttp.ClientResponse)
|
||||
fake_response.status = 200
|
||||
fake_response.json.return_value = fake_data
|
||||
service.session.get = mock.AsyncMock()
|
||||
service.session.get.return_value = fake_response
|
||||
|
||||
await service.update_weather()
|
||||
assert service._current_data.data == fake_data
|
||||
assert service._current_data.exception is None
|
||||
|
||||
service.session.get.assert_called_with(service.API_URL, params={
|
||||
'APPID': 'api_key',
|
||||
'q': characteristics['city'],
|
||||
'units': characteristics['units'],
|
||||
})
|
||||
assert fake_response.release.called is True
|
||||
|
||||
async def test_update_weather_non_ok_response(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.api_key = 'api_key'
|
||||
service.session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
|
||||
fake_response = mock.Mock(spec=aiohttp.ClientResponse)
|
||||
fake_response.status = 400
|
||||
fake_response.reason = 'Bad Request'
|
||||
fake_response.json.return_value = None
|
||||
service.session.get = mock.AsyncMock()
|
||||
service.session.get.return_value = fake_response
|
||||
|
||||
await service.update_weather()
|
||||
assert service._current_data.data is None
|
||||
assert service._current_data.exception is not None
|
||||
assert isinstance(service._current_data.exception, RuntimeError) is True
|
||||
assert service._current_data.exception.args[0] == 'HTTP 400 Bad Request'
|
||||
|
||||
assert fake_response.release.called is True
|
||||
|
||||
async def test_update_weather_request_exception(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.api_key = 'api_key'
|
||||
service.session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
|
||||
service.session.get = mock.AsyncMock()
|
||||
exception = RuntimeError('FIAL')
|
||||
service.session.get.side_effect = exception
|
||||
|
||||
await service.update_weather()
|
||||
assert service._current_data.data is None
|
||||
assert service._current_data.exception == exception
|
||||
|
||||
async def test_current_data(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service._current_data = ServiceData(data='spam')
|
||||
|
||||
result = await service.current_data()
|
||||
assert result is service._current_data
|
||||
|
||||
async def test_current_data_empty(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
|
||||
result = await service.current_data()
|
||||
assert result != service._current_data
|
||||
assert result.data is None
|
||||
assert result.exception is None
|
||||
|
||||
async def test_worker(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = True
|
||||
service.update_weather = mock.AsyncMock()
|
||||
service.notify = mock.AsyncMock()
|
||||
|
||||
with mock.patch('asyncio.sleep') as mock_asyncio_sleep:
|
||||
await service.worker()
|
||||
|
||||
mock_asyncio_sleep.assert_called_with(600)
|
||||
assert service.update_weather.called is True
|
||||
assert service.notify.called is True
|
||||
|
||||
async def test_start(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.update_weather = mock.AsyncMock()
|
||||
|
||||
fake_worker = mock.AsyncMock()
|
||||
service.worker = mock.Mock()
|
||||
service.worker.return_value = fake_worker
|
||||
|
||||
with mock.patch('aiojobs.aiohttp.get_scheduler_from_app') as mock_get_scheduler:
|
||||
with mock.patch('aiohttp.ClientSession') as mock_session:
|
||||
fake_scheduler = mock.Mock(spec=aiojobs._scheduler.Scheduler)
|
||||
fake_scheduler.spawn.return_value = mock.Mock(
|
||||
spec=aiojobs._job.Job,
|
||||
)
|
||||
mock_get_scheduler.return_value = fake_scheduler
|
||||
|
||||
fake_session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
mock_session.return_value = fake_session
|
||||
|
||||
await service.start()
|
||||
assert service.api_key == homehub_app['SETTINGS'].WEATHER_SERVICE_API_KEY
|
||||
assert service.session == fake_session
|
||||
assert service.update_weather.called is True
|
||||
mock_get_scheduler.assert_called_with(homehub_app)
|
||||
fake_scheduler.spawn.assert_called_with(fake_worker)
|
||||
|
||||
async def test_start_already_started(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.update_weather = mock.AsyncMock()
|
||||
|
||||
fake_worker = mock.AsyncMock()
|
||||
service.worker = mock.Mock()
|
||||
service.worker.return_value = fake_worker
|
||||
|
||||
with mock.patch('aiojobs.aiohttp.get_scheduler_from_app') as mock_get_scheduler:
|
||||
with mock.patch('aiohttp.ClientSession') as mock_session:
|
||||
fake_scheduler = mock.Mock(spec=aiojobs._scheduler.Scheduler)
|
||||
mock_get_scheduler.return_value = fake_scheduler
|
||||
|
||||
fake_session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
mock_session.return_value = fake_session
|
||||
|
||||
service._current_data = ServiceData(data='spam')
|
||||
service.session = fake_session
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
|
||||
await service.start()
|
||||
assert not mock_session.called
|
||||
assert not service.update_weather.called
|
||||
assert not mock_get_scheduler.called
|
||||
assert not fake_scheduler.spawn.called
|
||||
|
||||
async def test_stop(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
service.session.closed = False
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = False
|
||||
|
||||
await service.stop()
|
||||
assert service.session.close.called is True
|
||||
assert service.job.close.called is True
|
||||
|
||||
async def test_stop_already_stopped(self, homehub_app):
|
||||
characteristics = self._valid_characteristics()
|
||||
service = weather.WeatherService(
|
||||
homehub_app, 'testing', characteristics,
|
||||
)
|
||||
service.session = mock.Mock(spec=aiohttp.ClientSession)
|
||||
service.session.closed = True
|
||||
service.job = mock.Mock(spec=aiojobs._job.Job)
|
||||
service.job.closed = True
|
||||
|
||||
await service.stop()
|
||||
assert service.session.close.called is False
|
||||
assert service.job.close.called is False
|
||||
10
packages/homehub_backend/requirements-dev.txt
Normal file
10
packages/homehub_backend/requirements-dev.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
-r requirements.txt
|
||||
aiohttp-devtools==0.13.1
|
||||
flake8==3.8.3
|
||||
flake8-commas==2.0.0
|
||||
pycodestyle==2.6.0
|
||||
pytest==6.0.1
|
||||
pytest-aiohttp==0.3.0
|
||||
pytest-asyncio==0.14.0
|
||||
pytest-env==0.6.2
|
||||
twine==2.0.0
|
||||
2
packages/homehub_backend/requirements.txt
Normal file
2
packages/homehub_backend/requirements.txt
Normal file
@@ -0,0 +1,2 @@
|
||||
aiohttp==3.6.2
|
||||
aiojobs==0.2.2
|
||||
10
packages/homehub_backend/setup.cfg
Normal file
10
packages/homehub_backend/setup.cfg
Normal file
@@ -0,0 +1,10 @@
|
||||
[flake8]
|
||||
exclude = build/
|
||||
ignore = E402
|
||||
max-line-length = 120
|
||||
|
||||
[tool:pytest]
|
||||
testpaths =
|
||||
homehub_backend/tests
|
||||
env =
|
||||
HOMEHUB_SETTINGS_MODULE=homehub_backend.testing.settings
|
||||
57
packages/homehub_backend/setup.py
Executable file
57
packages/homehub_backend/setup.py
Executable file
@@ -0,0 +1,57 @@
|
||||
import io
|
||||
import os
|
||||
import re
|
||||
|
||||
from setuptools import find_packages
|
||||
from setuptools import setup
|
||||
|
||||
PROJECT_ROOT = os.path.dirname(__file__)
|
||||
|
||||
|
||||
def _process_requirements(requirements):
|
||||
return list(
|
||||
filter(lambda x: x != '', map(lambda x: x.strip(), requirements)),
|
||||
)
|
||||
|
||||
|
||||
def _process_packages(packages):
|
||||
return list(
|
||||
map(lambda x: 'homehub_backend.{package}'.format(package=x), packages),
|
||||
)
|
||||
|
||||
|
||||
with io.open('README.md', 'rt', encoding='utf8') as f:
|
||||
readme = f.read()
|
||||
|
||||
with io.open('requirements.txt', 'rt', encoding='utf8') as f:
|
||||
requirements = _process_requirements(f.read().split('\n'))
|
||||
|
||||
with io.open('homehub_backend/__init__.py', 'rt', encoding='utf8') as f:
|
||||
version = re.search(r"""__version__ = '(.*?)'""", f.read()).group(1)
|
||||
|
||||
packages = ['homehub_backend']
|
||||
packages.extend(_process_packages(find_packages(
|
||||
'homehub_backend', exclude=[
|
||||
'testing', 'testing.*', 'tests', 'tests.*',
|
||||
],
|
||||
)))
|
||||
|
||||
setup(
|
||||
name='homehub_backend',
|
||||
version=version,
|
||||
url='https://www.bthlabs.pl/',
|
||||
license='Apache License Version 2.0',
|
||||
author='BTHLabs',
|
||||
author_email='contact@bthlabs.pl',
|
||||
maintainer='BTHLabs',
|
||||
maintainer_email='contact@bthlabs.pl',
|
||||
description='BTHLabs HomeHub - Backend Application',
|
||||
long_description=readme,
|
||||
classifiers=[
|
||||
'License :: OSI Approved :: Apache Software License',
|
||||
],
|
||||
packages=packages,
|
||||
include_package_data=True,
|
||||
python_requires='>=3.7',
|
||||
install_requires=requirements,
|
||||
)
|
||||
Reference in New Issue
Block a user