Skip to content

Commit

Permalink
Merge pull request #46 from odin-detector/async_adapter
Browse files Browse the repository at this point in the history
Implement support for asynchronous adapters
  • Loading branch information
timcnicholls authored Apr 8, 2022
2 parents 366085b + c3a2514 commit 71ab2e6
Show file tree
Hide file tree
Showing 44 changed files with 4,677 additions and 1,021 deletions.
29 changes: 0 additions & 29 deletions .codeclimate.yml

This file was deleted.

14 changes: 14 additions & 0 deletions .coveragerc-py27
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[run]
omit =
*_version*
src/odin/adapters/async_adapter.py

[paths]
source=
src/
.tox/py*/lib/python*/site-packages/

[report]
omit =
*_version*
*/async_*.py
3 changes: 3 additions & 0 deletions .disable-travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ language: python
sudo: false
python:
- 2.7
- 3.6
- 3.7
- 3.8
- 3.9
addons:
apt:
packages:
Expand Down
7 changes: 6 additions & 1 deletion .github/workflows/test_odin_control.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,12 @@ jobs:
- name: Merge tox env specific coverage files
run: |
coverage combine
coverage xml
if [[ "${{ matrix.python-version }}" == 2.7* ]]; then
export COVERAGE_RC=.coveragerc-py27
else
export COVERAGE_RC=.coveragerc
fi
coverage xml --rcfile=$COVERAGE_RC
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v1
with:
Expand Down
12 changes: 1 addition & 11 deletions setup.cfg
Original file line number Diff line number Diff line change
@@ -1,14 +1,3 @@
[nosetests]
verbosity=2
nocapture=1
detailed-errors=1
with-coverage=1
cover-package=odin
cover-erase=1
#debug=nose.loader
#pdb=1
#pdb-failures=1

[flake8]
max-line-length = 100

Expand All @@ -18,3 +7,4 @@ style = pep440
versionfile_source = src/odin/_version.py
versionfile_build = odin/_version.py
tag_prefix=

25 changes: 22 additions & 3 deletions src/odin/adapters/adapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import logging

from odin.util import wrap_result

class ApiAdapter(object):
"""
Expand All @@ -16,11 +17,14 @@ class ApiAdapter(object):
implement them, returning an error message and 400 code.
"""

is_async = False

def __init__(self, **kwargs):
"""Initialise the ApiAdapter object.
:param kwargs: keyword argument list that is copied into options dictionary
"""
super(ApiAdapter, self).__init__()
self.name = type(self).__name__

# Load any keyword arguments into the adapter options dictionary
Expand Down Expand Up @@ -51,6 +55,20 @@ def get(self, path, request):
response = "GET method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)

def post(self, path, request):
"""Handle an HTTP POST request.
This method is an abstract implementation of the POST request handler for ApiAdapter.
:param path: URI path of resource
:param request: HTTP request object passed from handler
:return: ApiAdapterResponse container of data, content-type and status_code
"""
logging.debug('POST on path %s from %s: method not implemented by %s',
path, request.remote_ip, self.name)
response = "POST method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)

def put(self, path, request):
"""Handle an HTTP PUT request.
Expand Down Expand Up @@ -200,9 +218,10 @@ def wrapper(_self, path, request):
# Validate the Content-Type header in the request against allowed types
if 'Content-Type' in request.headers:
if request.headers['Content-Type'] not in oargs:
return ApiAdapterResponse(
response = ApiAdapterResponse(
'Request content type ({}) not supported'.format(
request.headers['Content-Type']), status_code=415)
return wrap_result(response, _self.is_async)
return func(_self, path, request)
return wrapper
return decorator
Expand Down Expand Up @@ -254,10 +273,10 @@ def wrapper(_self, path, request):
# If it was not possible to resolve a response type or there was not default
# given, return an error code 406
if response_type is None:
return ApiAdapterResponse(
response = ApiAdapterResponse(
"Requested content types not supported", status_code=406
)

return wrap_result(response, _self.is_async)
else:
response_type = okwargs['default'] if 'default' in okwargs else 'text/plain'
request.headers['Accept'] = response_type
Expand Down
123 changes: 123 additions & 0 deletions src/odin/adapters/async_adapter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
"""
odin.adapters.adapter.py - base asynchronous API adapter implmentation for the ODIN server.
Tim Nicholls, STFC Detector Systems Software Group
"""

import asyncio
import logging
import inspect

from odin.adapters.adapter import ApiAdapter, ApiAdapterResponse


class AsyncApiAdapter(ApiAdapter):
"""
Asynchronous API adapter base class.
This class defines the basis for all async API adapters and provides default
methods for the required HTTP verbs in case the derived classes fail to
implement them, returning an error message and 400 code.
"""

is_async = True

def __init__(self, **kwargs):
"""Initialise the AsyncApiAdapter object.
:param kwargs: keyword argument list that is copied into options dictionary
"""
super(AsyncApiAdapter, self).__init__(**kwargs)

def __await__(self):
"""Make AsyncApiAdapter objects awaitable.
This magic method makes the instantiation of AsyncApiAdapter objects awaitable. This allows
any underlying async and awaitable attributes, e.g. an AsyncParameterTree, to be correctly
awaited when the adapter is loaded."""
async def closure():
"""Await all async attributes of the adapter."""
awaitable_attrs = [attr for attr in self.__dict__.values() if inspect.isawaitable(attr)]
await asyncio.gather(*awaitable_attrs)
return self

return closure().__await__()

async def initialize(self, adapters):
"""Initialize the AsyncApiAdapter after it has been registered by the API Route.
This is an abstract implementation of the initialize mechinism that allows
an adapter to receive a list of loaded adapters, for Inter-adapter communication.
:param adapters: a dictionary of the adapters loaded by the API route.
"""

pass

async def cleanup(self):
"""Clean up adapter state.
This is an abstract implementation of the cleanup mechanism provided to allow adapters
to clean up their state (e.g. disconnect cleanly from the device being controlled, set
some status message).
"""
pass

async def get(self, path, request):
"""Handle an HTTP GET request.
This method is an abstract implementation of the GET request handler for AsyncApiAdapter.
:param path: URI path of resource
:param request: HTTP request object passed from handler
:return: ApiAdapterResponse container of data, content-type and status_code
"""
logging.debug('GET on path %s from %s: method not implemented by %s',
path, request.remote_ip, self.name)
await asyncio.sleep(0)
response = "GET method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)

async def post(self, path, request):
"""Handle an HTTP POST request.
This method is an abstract implementation of the POST request handler for AsyncApiAdapter.
:param path: URI path of resource
:param request: HTTP request object passed from handler
:return: ApiAdapterResponse container of data, content-type and status_code
"""
logging.debug('POST on path %s from %s: method not implemented by %s',
path, request.remote_ip, self.name)
await asyncio.sleep(0)
response = "POST method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)

async def put(self, path, request):
"""Handle an HTTP PUT request.
This method is an abstract implementation of the PUT request handler for AsyncApiAdapter.
:param path: URI path of resource
:param request: HTTP request object passed from handler
:return: ApiAdapterResponse container of data, content-type and status_code
"""
logging.debug('PUT on path %s from %s: method not implemented by %s',
path, request.remote_ip, self.name)
await asyncio.sleep(0)
response = "PUT method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)

async def delete(self, path, request):
"""Handle an HTTP DELETE request.
This method is an abstract implementation of the DELETE request handler for ApiAdapter.
:param path: URI path of resource
:param request: HTTP request object passed from handler
:return: ApiAdapterResponse container of data, content-type and status_code
"""
logging.debug('DELETE on path %s from %s: method not implemented by %s',
path, request.remote_ip, self.name)
await asyncio.sleep(0)
response = "DELETE method not implemented by {}".format(self.name)
return ApiAdapterResponse(response, status_code=400)
Loading

0 comments on commit 71ab2e6

Please sign in to comment.